diff --git a/.gitignore b/.gitignore index 1ac6cad..8c5c3bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ foodpantryserver.zip +sites +static/css/uikit-rtl.css +static/css/uikit-rtl.min.css +static/css/uikit.css +static/css/uikit.min.css \ No newline at end of file diff --git a/MyDataclasses.py b/MyDataclasses.py index 704bd95..54d85a2 100644 --- a/MyDataclasses.py +++ b/MyDataclasses.py @@ -63,18 +63,6 @@ class FoodInfoPayload: self.default_expiration ) -@dataclass -class BrandsPayload: - name: str - - def __post_init__(self): - if not isinstance(self.name, str): - return TypeError(f"brand name should be of type str; not {type(self.name)}") - - def payload(self): - return ( - self.name, - ) @dataclass class ItemsPayload: @@ -107,27 +95,7 @@ class ItemsPayload: self.item_type, self.search_string ) - -@dataclass -class ItemLocationPayload: - part_id: int - location_id: int - quantity_on_hand: float = 0.0 - cost_layers: list = field(default_factory=list) - def __post_init__(self): - if not isinstance(self.part_id, int): - raise TypeError(f"part_id must be of type int; not {type(self.part_id)}") - if not isinstance(self.location_id, int): - raise TypeError(f"part_id must be of type int; not {type(self.part_id)}") - - def payload(self): - return ( - self.part_id, - self.location_id, - self.quantity_on_hand, - lst2pgarr(self.cost_layers) - ) @dataclass class TransactionPayload: @@ -173,65 +141,6 @@ class CostLayerPayload: self.vendor ) -@dataclass -class LocationPayload: - uuid: str - name: str - zone_id: int - - def __post_init__(self): - if not isinstance(self.uuid, str): - raise TypeError(f"uuid must be of type str; not {type(self.uuid)}") - if not isinstance(self.name, str): - raise TypeError(f"Location name must be of type str; not {type(self.name)}") - if not isinstance(self.zone_id, int): - raise TypeError(f"zone_id must be of type str; not {type(self.zone_id)}") - - def payload(self): - return ( - self.uuid, - self.name, - self.zone_id - ) - -@dataclass -class ZonePayload: - name: str - site_id: int - - def __post_init__(self): - if not isinstance(self.name, str): - raise TypeError(f"Zone name should be of type str; not {type(self.name)}") - - def payload(self): - return ( - self.name, - self.site_id - ) - -@dataclass -class VendorPayload: - vendor_name: str - created_by: int - vendor_address: str = "" - creation_date: datetime.datetime = field(init=False) - phone_number: str = "" - - def __post_init__(self): - if not isinstance(self.vendor_name, str): - raise TypeError(f"vendor_name should be of type str; not {type(self.vendor_name)}") - self.creation_date = datetime.datetime.now() - - - def payload(self): - return ( - self.vendor_name, - self.vendor_address, - self.creation_date, - self.created_by, - self.phone_number - ) - @dataclass class ItemLinkPayload: barcode: str diff --git a/__pycache__/MyDataclasses.cpython-312.pyc b/__pycache__/MyDataclasses.cpython-312.pyc index 23bcdf5..1c402a2 100644 Binary files a/__pycache__/MyDataclasses.cpython-312.pyc and b/__pycache__/MyDataclasses.cpython-312.pyc differ diff --git a/__pycache__/database.cpython-312.pyc b/__pycache__/database.cpython-312.pyc index a3f88cc..3aa5ad5 100644 Binary files a/__pycache__/database.cpython-312.pyc and b/__pycache__/database.cpython-312.pyc differ diff --git a/__pycache__/external_API.cpython-312.pyc b/__pycache__/external_API.cpython-312.pyc index 20700ee..98e1ac3 100644 Binary files a/__pycache__/external_API.cpython-312.pyc and b/__pycache__/external_API.cpython-312.pyc differ diff --git a/__pycache__/item_API.cpython-312.pyc b/__pycache__/item_API.cpython-312.pyc index b21e2dc..ce49df8 100644 Binary files a/__pycache__/item_API.cpython-312.pyc and b/__pycache__/item_API.cpython-312.pyc differ diff --git a/__pycache__/postsqldb.cpython-312.pyc b/__pycache__/postsqldb.cpython-312.pyc index ff8dc99..c94c452 100644 Binary files a/__pycache__/postsqldb.cpython-312.pyc and b/__pycache__/postsqldb.cpython-312.pyc differ diff --git a/__pycache__/process.cpython-312.pyc b/__pycache__/process.cpython-312.pyc index 0379a10..7a232e0 100644 Binary files a/__pycache__/process.cpython-312.pyc and b/__pycache__/process.cpython-312.pyc differ diff --git a/__pycache__/receipts_API.cpython-312.pyc b/__pycache__/receipts_API.cpython-312.pyc index f59419a..9202ebe 100644 Binary files a/__pycache__/receipts_API.cpython-312.pyc and b/__pycache__/receipts_API.cpython-312.pyc differ diff --git a/__pycache__/recipes_api.cpython-312.pyc b/__pycache__/recipes_api.cpython-312.pyc index a6b056e..ecc20d8 100644 Binary files a/__pycache__/recipes_api.cpython-312.pyc and b/__pycache__/recipes_api.cpython-312.pyc differ diff --git a/__pycache__/webpush.cpython-312.pyc b/__pycache__/webpush.cpython-312.pyc new file mode 100644 index 0000000..34fe056 Binary files /dev/null and b/__pycache__/webpush.cpython-312.pyc differ diff --git a/__pycache__/webserver.cpython-312.pyc b/__pycache__/webserver.cpython-312.pyc index af3ec6c..fc5a7e2 100644 Binary files a/__pycache__/webserver.cpython-312.pyc and b/__pycache__/webserver.cpython-312.pyc differ diff --git a/__pycache__/workshop_api.cpython-312.pyc b/__pycache__/workshop_api.cpython-312.pyc new file mode 100644 index 0000000..6bcea1e Binary files /dev/null and b/__pycache__/workshop_api.cpython-312.pyc differ diff --git a/database.log b/database.log index edd4e96..c0ee631 100644 --- a/database.log +++ b/database.log @@ -1718,4 +1718,94 @@ sql='INSERT INTO main_recipe_items(uuid, rp_id, item_type, item_name, uom, qty, item_id, links) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) RETURNING *;') 2025-04-12 19:43:45.390676 --- ERROR --- DatabaseError(message='can't adapt type 'dict'', payload=('%024600017008%', 1, 'sku', 'Kosher salt', {'id': 1, 'plural': 'pinches', 'single': ' pinch', 'fullname': ' Pinch', 'description': ' Less than 1/8 teaspoon.'}, 1.0, 141, '{}'), - sql='INSERT INTO main_recipe_items(uuid, rp_id, item_type, item_name, uom, qty, item_id, links) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) RETURNING *;') \ No newline at end of file + sql='INSERT INTO main_recipe_items(uuid, rp_id, item_type, item_name, uom, qty, item_id, links) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) RETURNING *;') +2025-04-13 10:06:16.130857 --- ERROR --- DatabaseError(message='unsupported format character ';' (0x3b) at index 42', + payload=(), + sql='SELECT * FROM test_zones LIMIT %s OFFSET %;') +2025-04-17 08:07:14.828153 --- ERROR --- DatabaseError(message='invalid input syntax for type integer: "ShelfA"LINE 17: @ShelfA', '6', 'ShelfA') ^', + payload=('\n \n \n \n \n \n \n \n \n \n \n \n \n \n @ShelfA', '6', 'ShelfA'), + sql='INSERT INTO test_zones(name, description, site_id) VALUES (%s, %s, %s) RETURNING *;') +2025-04-17 08:09:29.519938 --- ERROR --- DatabaseError(message='invalid input syntax for type integer: "ShelfA"LINE 17: @ShelfA', '6', 'ShelfA') ^', + payload=('\n \n \n \n \n \n \n \n \n \n \n \n \n \n @ShelfA', '6', 'ShelfA'), + sql='INSERT INTO test_zones(name, description, site_id) VALUES (%s, %s, %s) RETURNING *;') +2025-04-17 08:12:23.901770 --- ERROR --- DatabaseError(message='value too long for type character varying(32)', + payload=('\n \n \n \n \n \n \n \n \n \n \n \n \n \n ', 'test', 2), + sql='INSERT INTO test_zones(name, description, site_id) VALUES (%s, %s, %s) RETURNING *;') +2025-04-17 08:13:11.019738 --- ERROR --- DatabaseError(message='value too long for type character varying(32)', + payload=('\n \n \n \n \n \n \n \n \n \n \n \n \n \n ', 'test', 2), + sql='INSERT INTO test_zones(name, description, site_id) VALUES (%s, %s, %s) RETURNING *;') +2025-04-17 08:15:02.629959 --- ERROR --- DatabaseError(message='duplicate key value violates unique constraint "test_zones_name_key"DETAIL: Key (name)=(KITCHEN) already exists.', + payload=('KITCHEN', 'test', 2), + sql='INSERT INTO test_zones(name, description, site_id) VALUES (%s, %s, %s) RETURNING *;') +2025-04-17 08:16:00.387539 --- ERROR --- DatabaseError(message='insert or update on table "test_zones" violates foreign key constraint "fk_site"DETAIL: Key (site_id)=(2) is not present in table "sites".', + payload=('KITCHEN@test', 'test', 2), + sql='INSERT INTO test_zones(name, description, site_id) VALUES (%s, %s, %s) RETURNING *;') +2025-04-17 08:16:53.559749 --- ERROR --- DatabaseError(message='insert or update on table "test_zones" violates foreign key constraint "fk_site"DETAIL: Key (site_id)=(2) is not present in table "sites".', + payload=('KITCHEN@Fridge', 'Fridge', 2), + sql='INSERT INTO test_zones(name, description, site_id) VALUES (%s, %s, %s) RETURNING *;') +2025-04-17 13:33:53.682228 --- ERROR --- DatabaseError(message='tuple index out of range', + payload=(), + sql='INSERT INTO test_locations(uuid, name, zone_id) VALUES (%s, %s, %s) RETURNING *;') +2025-04-17 17:35:40.178418 --- ERROR --- DatabaseError(message='syntax error at or near "20"LINE 1: WITH 20 AS passed_id, ^', + payload=(20, 20, 0), + sql='WITH %s AS passed_id, cte_item_locations AS ( SELECT DISTINCT ils.zone_id FROM test_item_locations ils WHERE ils.part_id = passed_id; )SELECT DISTINCT zone.* FROM cte_item_locations cilJOIN test_zones zone ON cil.zone_id = zone.idLIMIT %s OFFSET %s;') +2025-04-17 17:36:49.614637 --- ERROR --- DatabaseError(message='syntax error at or near ";"LINE 4: WHERE ils.part_id = passed_id; ^', + payload=(20, 20, 0), + sql='WITH passed_id AS (SELECT %s as passed_id), cte_item_locations AS ( SELECT DISTINCT ils.zone_id FROM test_item_locations ils WHERE ils.part_id = passed_id; )SELECT DISTINCT zone.* FROM cte_item_locations cilJOIN test_zones zone ON cil.zone_id = zone.idLIMIT %s OFFSET %s;') +2025-04-17 17:37:05.249411 --- ERROR --- DatabaseError(message='column ils.zone_id does not existLINE 3: SELECT DISTINCT ils.zone_id FROM test_item_locations... ^', + payload=(20, 20, 0), + sql='WITH passed_id AS (SELECT %s as passed_id), cte_item_locations AS ( SELECT DISTINCT ils.zone_id FROM test_item_locations ils WHERE ils.part_id = passed_id )SELECT DISTINCT zone.* FROM cte_item_locations cilJOIN test_zones zone ON cil.zone_id = zone.idLIMIT %s OFFSET %s;') +2025-04-17 17:48:36.064425 --- ERROR --- DatabaseError(message='column "passed_id" does not existLINE 4: WHERE ils.part_id = passed_id ^', + payload=(20, 20, 0), + sql='WITH passed_id AS (SELECT %s as passed_id), cte_item_locations AS ( SELECT DISTINCT ils.location_id FROM test_item_locations ils WHERE ils.part_id = passed_id ), cte_locations AS ( SELECT DISTINCT locations.zone_id FROM test_locations locations WHERE locations.id IN (SELECT location_id FROM cte_item_locations) )SELECT DISTINCT zone.* FROM cte_locations cilJOIN test_zones zone ON cil.zone_id = zone.idLIMIT %s OFFSET %s;') +2025-04-17 17:48:59.073548 --- ERROR --- DatabaseError(message='syntax error at or near "20"LINE 1: WITH 20 AS passed_id, ^', + payload=(20, 20, 0), + sql='WITH passed_id AS (SELECT %s as passed_id), cte_item_locations AS ( SELECT DISTINCT ils.location_id FROM test_item_locations ils WHERE ils.part_id = (SELECT passed_id FROM passed_id) ), cte_locations AS ( SELECT DISTINCT locations.zone_id FROM test_locations locations WHERE locations.id IN (SELECT location_id FROM cte_item_locations) )SELECT DISTINCT zone.* FROM cte_locations cilJOIN test_zones zone ON cil.zone_id = zone.idLIMIT %s OFFSET %s;') +2025-04-17 17:50:47.491564 --- ERROR --- DatabaseError(message='syntax error at or near "20"LINE 1: WITH 20 AS passed_id, ^', + payload=(20, 20, 0), + sql='WITH passed_id AS (SELECT %s AS passed_id), cte_item_locations AS ( SELECT DISTINCT ils.location_id FROM test_item_locations ils WHERE ils.part_id = (SELECT passed_id FROM passed_id) ), cte_locations AS ( SELECT DISTINCT locations.zone_id FROM test_locations locations WHERE locations.id IN (SELECT location_id FROM cte_item_locations) )SELECT DISTINCT zone.* FROM cte_locations cilJOIN test_zones zone ON cil.zone_id = zone.idLIMIT %s OFFSET %s;') +2025-04-19 15:30:08.126535 --- ERROR --- DatabaseError(message='duplicate key value violates unique constraint "test_receipts_receipt_id_key"DETAIL: Key (receipt_id)=(SIR-00000012) already exists.', + payload=('SIR-00000012', 'Unresolved', datetime.datetime(2025, 4, 19, 15, 29, 27, 691584), 1, 1, '{}'), + sql='INSERT INTO test_receipts(receipt_id, receipt_status, date_submitted, submitted_by, vendor_id, files) VALUES (%s, %s, %s, %s, %s, %s) RETURNING *;') +2025-04-19 16:06:21.543923 --- ERROR --- DatabaseError(message=''int' object is not iterable', + payload=(25, 0), + sql='SELECT * FROM test_items WHERE row_type = 'list' LIMIT %s OFFSET %s;') +2025-04-19 18:57:04.630250 --- ERROR --- DatabaseError(message='duplicate key value violates unique constraint "test_logistics_info_barcode_key"DETAIL: Key (barcode)=(%6111031005064%) already exists.', + payload=('%6111031005064%', 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-04-19 19:30:10.634447 --- ERROR --- DatabaseError(message='syntax error at or near "'total_qoh'"LINE 16: ORDER BY test_items.'total_qoh' ASC ^', + payload=('', 'total_qoh', 50, 200), + sql='WITH sum_cte AS ( SELECT mi.id, SUM(mil.quantity_on_hand)::FLOAT8 AS total_sum FROM test_item_locations mil JOIN test_items mi ON mil.part_id = mi.id GROUP BY mi.id )SELECT test_items.*, row_to_json(test_item_info.*) as item_info, sum_cte.total_sum as total_qoh, (SELECT COALESCE(row_to_json(u), '{}') FROM units as u WHERE u.id=test_item_info.uom) as uomFROM test_itemsLEFT JOIN sum_cte ON test_items.id = sum_cte.idLEFT JOIN test_item_info ON test_items.item_info_id = test_item_info.idWHERE test_items.search_string LIKE '%%' || %s || '%%'ORDER BY test_items.%s ASCLIMIT %s OFFSET %s;') +2025-04-19 19:30:46.113871 --- ERROR --- DatabaseError(message='not all arguments converted during string formatting', + payload=('', 'id', 50, 0), + sql='WITH sum_cte AS ( SELECT mi.id, SUM(mil.quantity_on_hand)::FLOAT8 AS total_sum FROM test_item_locations mil JOIN test_items mi ON mil.part_id = mi.id GROUP BY mi.id )SELECT test_items.*, row_to_json(test_item_info.*) as item_info, sum_cte.total_sum as total_qoh, (SELECT COALESCE(row_to_json(u), '{}') FROM units as u WHERE u.id=test_item_info.uom) as uomFROM test_itemsLEFT JOIN sum_cte ON test_items.id = sum_cte.idLEFT JOIN test_item_info ON test_items.item_info_id = test_item_info.idWHERE test_items.search_string LIKE '%%' || %s || '%%'ORDER BY test_items.? ASCLIMIT %s OFFSET %s;') +2025-04-19 19:32:19.050185 --- ERROR --- DatabaseError(message='syntax error at or near "'id'"LINE 16: ORDER BY test_items.'id' ASC ^', + payload=('', 'id', 50, 0), + sql='WITH sum_cte AS ( SELECT mi.id, SUM(mil.quantity_on_hand)::FLOAT8 AS total_sum FROM test_item_locations mil JOIN test_items mi ON mil.part_id = mi.id GROUP BY mi.id )SELECT test_items.*, row_to_json(test_item_info.*) as item_info, sum_cte.total_sum as total_qoh, (SELECT COALESCE(row_to_json(u), '{}') FROM units as u WHERE u.id=test_item_info.uom) as uomFROM test_itemsLEFT JOIN sum_cte ON test_items.id = sum_cte.idLEFT JOIN test_item_info ON test_items.item_info_id = test_item_info.idWHERE test_items.search_string LIKE '%%' || %s || '%%'ORDER BY test_items.%s ASCLIMIT %s OFFSET %s;') +2025-04-19 19:34:18.066682 --- ERROR --- DatabaseError(message='not all arguments converted during string formatting', + payload=('', 50, 0, 'id'), + sql='WITH sum_cte AS ( SELECT mi.id, SUM(mil.quantity_on_hand)::FLOAT8 AS total_sum FROM test_item_locations mil JOIN test_items mi ON mil.part_id = mi.id GROUP BY mi.id )SELECT test_items.*, row_to_json(test_item_info.*) as item_info, sum_cte.total_sum as total_qoh, (SELECT COALESCE(row_to_json(u), '{}') FROM units as u WHERE u.id=test_item_info.uom) as uomFROM test_itemsLEFT JOIN sum_cte ON test_items.id = sum_cte.idLEFT JOIN test_item_info ON test_items.item_info_id = test_item_info.idWHERE test_items.search_string LIKE '%%' || %s || '%%'ORDER BY test_items.id ASCLIMIT %s OFFSET %s;') +2025-04-19 19:35:13.155663 --- ERROR --- DatabaseError(message='string index out of range', + payload=id, + sql='WITH sum_cte AS ( SELECT mi.id, SUM(mil.quantity_on_hand)::FLOAT8 AS total_sum FROM test_item_locations mil JOIN test_items mi ON mil.part_id = mi.id GROUP BY mi.id )SELECT test_items.*, row_to_json(test_item_info.*) as item_info, sum_cte.total_sum as total_qoh, (SELECT COALESCE(row_to_json(u), '{}') FROM units as u WHERE u.id=test_item_info.uom) as uomFROM test_itemsLEFT JOIN sum_cte ON test_items.id = sum_cte.idLEFT JOIN test_item_info ON test_items.item_info_id = test_item_info.idWHERE test_items.search_string LIKE '%%' || %s || '%%'ORDER BY test_items.id ASCLIMIT %s OFFSET %s;') +2025-04-19 19:35:48.262338 --- ERROR --- DatabaseError(message='string index out of range', + payload=id, + sql='WITH sum_cte AS ( SELECT mi.id, SUM(mil.quantity_on_hand)::FLOAT8 AS total_sum FROM test_item_locations mil JOIN test_items mi ON mil.part_id = mi.id GROUP BY mi.id )SELECT test_items.*, row_to_json(test_item_info.*) as item_info, sum_cte.total_sum as total_qoh, (SELECT COALESCE(row_to_json(u), '{}') FROM units as u WHERE u.id=test_item_info.uom) as uomFROM test_itemsLEFT JOIN sum_cte ON test_items.id = sum_cte.idLEFT JOIN test_item_info ON test_items.item_info_id = test_item_info.idWHERE test_items.search_string LIKE '%%' || %s || '%%'ORDER BY test_items.id ASCLIMIT %s OFFSET %s;') +2025-04-19 19:36:33.407176 --- ERROR --- DatabaseError(message='column test_items.total_qoh does not existLINE 16: ORDER BY test_items.total_qoh ASC ^', + payload=['', 50, 100], + sql='WITH sum_cte AS ( SELECT mi.id, SUM(mil.quantity_on_hand)::FLOAT8 AS total_sum FROM test_item_locations mil JOIN test_items mi ON mil.part_id = mi.id GROUP BY mi.id )SELECT test_items.*, row_to_json(test_item_info.*) as item_info, sum_cte.total_sum as total_qoh, (SELECT COALESCE(row_to_json(u), '{}') FROM units as u WHERE u.id=test_item_info.uom) as uomFROM test_itemsLEFT JOIN sum_cte ON test_items.id = sum_cte.idLEFT JOIN test_item_info ON test_items.item_info_id = test_item_info.idWHERE test_items.search_string LIKE '%%' || %s || '%%'ORDER BY test_items.total_qoh ASCLIMIT %s OFFSET %s;') +2025-04-19 19:37:57.421608 --- ERROR --- DatabaseError(message='column test_items.total_qoh does not existLINE 16: ORDER BY test_items.total_qoh ASC ^', + payload=['', 50, 50], + sql='WITH sum_cte AS ( SELECT mi.id, SUM(mil.quantity_on_hand)::FLOAT8 AS total_sum FROM test_item_locations mil JOIN test_items mi ON mil.part_id = mi.id GROUP BY mi.id )SELECT test_items.*, row_to_json(test_item_info.*) as item_info, sum_cte.total_sum as total_qoh, (SELECT COALESCE(row_to_json(u), '{}') FROM units as u WHERE u.id=test_item_info.uom) as uomFROM test_itemsLEFT JOIN sum_cte ON test_items.id = sum_cte.idLEFT JOIN test_item_info ON test_items.item_info_id = test_item_info.idWHERE test_items.search_string LIKE '%%' || %s || '%%'ORDER BY test_items.total_qoh ASCLIMIT %s OFFSET %s;') +2025-04-19 20:14:49.445587 --- ERROR --- DatabaseError(message='syntax error at or near "item_name"LINE 16: ORDER BY main_items.id item_name ^', + payload=['', 50, 0], + sql='WITH sum_cte AS ( SELECT mi.id, 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 )SELECT main_items.*, row_to_json(main_item_info.*) as item_info, sum_cte.total_sum as total_qoh, (SELECT COALESCE(row_to_json(u), '{}') FROM units as u WHERE u.id=main_item_info.uom) as uomFROM main_itemsLEFT JOIN sum_cte ON main_items.id = sum_cte.idLEFT JOIN main_item_info ON main_items.item_info_id = main_item_info.idWHERE main_items.search_string LIKE '%%' || %s || '%%'ORDER BY main_items.id item_nameLIMIT %s OFFSET %s;') +2025-04-19 20:16:09.867711 --- ERROR --- DatabaseError(message='syntax error at or near "total_qoh"LINE 16: ORDER BY main_items.id total_qoh ^', + payload=['', 50, 0], + sql='WITH sum_cte AS ( SELECT mi.id, 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 )SELECT main_items.*, row_to_json(main_item_info.*) as item_info, sum_cte.total_sum as total_qoh, (SELECT COALESCE(row_to_json(u), '{}') FROM units as u WHERE u.id=main_item_info.uom) as uomFROM main_itemsLEFT JOIN sum_cte ON main_items.id = sum_cte.idLEFT JOIN main_item_info ON main_items.item_info_id = main_item_info.idWHERE main_items.search_string LIKE '%%' || %s || '%%'ORDER BY main_items.id total_qohLIMIT %s OFFSET %s;') +2025-04-19 20:16:38.335617 --- ERROR --- DatabaseError(message='syntax error at or near "item_name"LINE 16: ORDER BY main_items.id item_name ^', + payload=['', 50, 0], + sql='WITH sum_cte AS ( SELECT mi.id, 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 )SELECT main_items.*, row_to_json(main_item_info.*) as item_info, sum_cte.total_sum as total_qoh, (SELECT COALESCE(row_to_json(u), '{}') FROM units as u WHERE u.id=main_item_info.uom) as uomFROM main_itemsLEFT JOIN sum_cte ON main_items.id = sum_cte.idLEFT JOIN main_item_info ON main_items.item_info_id = main_item_info.idWHERE main_items.search_string LIKE '%%' || %s || '%%'ORDER BY main_items.id item_nameLIMIT %s OFFSET %s;') +2025-04-20 09:54:33.948670 --- ERROR --- DatabaseError(message=''int' object is not iterable', + payload=(), + sql='SELECT * FROM sites') \ No newline at end of file diff --git a/database.py b/database.py index f0bce71..8441094 100644 --- a/database.py +++ b/database.py @@ -1597,7 +1597,10 @@ def getItemsWithQOH(conn, site, payload, convert=True): recordset = [] count = 0 with open(f"sql/SELECT/getItemsWithQOH.sql", "r+") as file: - sql = file.read().replace("%%site_name%%", site) + sql = file.read().replace("%%site_name%%", site).replace("%%sort_order%%", payload[3]) + + payload = list(payload) + payload.pop(3) try: if convert: with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: diff --git a/external_API.py b/external_API.py index e0fde2c..64ac9a6 100644 --- a/external_API.py +++ b/external_API.py @@ -6,6 +6,7 @@ from threading import Thread from queue import Queue import time, process from user_api import login_required +import webpush external_api = Blueprint('external', __name__) @@ -22,7 +23,6 @@ def getItemLocations(): database_config = config() with psycopg2.connect(**database_config) as conn: recordset, count = database.getItemLocations(conn, site_name, (item_id, limit, offset), convert=True) - print(count) return jsonify({"locations":recordset, "end":math.ceil(count/limit), "error":False, "message":"item fetched succesfully!"}) return jsonify({"locations":recordset, "end": math.ceil(count/limit), "error":True, "message":"There was an error with this GET statement"}) @@ -47,8 +47,6 @@ def getItemBarcode(): database_config = config() with psycopg2.connect(**database_config) as conn: record = database.getItemAllByBarcode(conn, site_name, (item_barcode, ), convert=True) - - print(record) if record == {}: return jsonify({"item":None, "error":True, "message":"Item either does not exist or there was a larger problem!"}) else: @@ -116,6 +114,7 @@ def post_receipt(): data=item['item']['data'] ) database.insertReceiptItemsTuple(conn, site_name, receipt_item.payload()) - + #webpush.push_notifications('New Receipt', f"Receipt {receipt['receipt_id']} was added to Site -> {site_name}!") + webpush.push_ntfy('New Receipt', f"Receipt {receipt['receipt_id']} was added to Site -> {site_name}!") return jsonify({"error":False, "message":"Transaction Complete!"}) return jsonify({"error":True, "message":"There was an error with this POST statement"}) \ No newline at end of file diff --git a/instance/application.cfg.py b/instance/application.cfg.py new file mode 100644 index 0000000..c18dd2c --- /dev/null +++ b/instance/application.cfg.py @@ -0,0 +1,3 @@ +VAPID_PUBLIC_KEY = "BIbaHOqZcTunzAeb9p1hCWBo1DeJN0NVf2WVNSrsLZ2e50vhBno5dJuRAB1NLNXIeeQYr_x-1fSifJBGfUKd6QM" +VAPID_PRIVATE_KEY = "l2wLDKWJDMkytDSN8s9iu9Y3-5qrIMy_OreUC3vDHtI" +VAPID_CLAIM_EMAIL = "jadowyne.ulve@outlook.com" \ No newline at end of file diff --git a/item_API.py b/item_API.py index 50c2ae4..b4ebcab 100644 --- a/item_API.py +++ b/item_API.py @@ -63,14 +63,20 @@ def pagninate_items(): page = int(request.args.get('page', 1)) limit = int(request.args.get('limit', 10)) search_string = str(request.args.get('search_text', "")) - sort_order = request.args.get('sort_order', "") + sort = request.args.get('sort', "") + order = request.args.get('order', "") + view = request.args.get('view', "") site_name = session['selected_site'] offset = (page - 1) * limit - + if sort == 'total_qoh': + sort_order = f"{sort} {order}" + else: + sort_order = f"{site_name}_items.{sort} {order}" + print(sort_order) database_config = config() with psycopg2.connect(**database_config) as conn: - pantry_inventory, count = database.getItemsWithQOH(conn, site_name, (search_string, limit, offset), convert=True) + pantry_inventory, count = database.getItemsWithQOH(conn, site_name, (search_string, limit, offset, sort_order), convert=True) return jsonify({'items': pantry_inventory, "end": math.ceil(count['count']/limit), 'error':False, 'message': 'Items Loaded Successfully!'}) return jsonify({'items': pantry_inventory, "end": math.ceil(count['count']/limit), 'error':True, 'message': 'There was a problem loading the items!'}) @@ -106,7 +112,7 @@ def getModalPrefixes(): database_config = config() with psycopg2.connect(**database_config) as conn: payload = (limit, offset) - recordset, count = postsqldb.SKUPrefixTable.getPrefixes(conn, site_name, payload, convert=True) + recordset, count = postsqldb.SKUPrefixTable.paginatePrefixes(conn, site_name, payload, convert=True) return jsonify({"prefixes":recordset, "end":math.ceil(count/limit), "error":False, "message":"items fetched succesfully!"}) return jsonify({"prefixes":recordset, "end":math.ceil(count/limit), "error":True, "message":"There was an error with this GET statement"}) @@ -126,9 +132,46 @@ def getZones(): print(count, len(zones)) return jsonify(zones=zones, endpage=math.ceil(count[0]/limit)) + +@items_api.route('/item/getZonesBySku', methods=["GET"]) +def getZonesbySku(): + if request.method == "GET": + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 1)) + item_id = int(request.args.get('item_id')) + database_config = config() + site_name = session['selected_site'] + zones = [] + offset = (page - 1) * limit + payload = (item_id, limit, offset) + count = 0 + with psycopg2.connect(**database_config) as conn: + zones, count = postsqldb.ZonesTable.paginateZonesBySku(conn, site_name, payload) + print(zones, count) + return jsonify(zones=zones, endpage=math.ceil(count/limit)) + +@items_api.route('/item/getLocationsBySkuZone', methods=['get']) +def getLocationsBySkuZone(): + zone_id = int(request.args.get('zone_id', 1)) + part_id = int(request.args.get('part_id', 1)) + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 1)) + + offset = (page-1)*limit + database_config = config() + site_name = session['selected_site'] + locations = [] + count=0 + with psycopg2.connect(**database_config) as conn: + payload = (part_id, zone_id, limit, offset) + locations, count = postsqldb.LocationsTable.paginateLocationsBySkuZone(conn, site_name, payload) + return jsonify(locations=locations, endpage=math.ceil(count/limit)) + + @items_api.route('/item/getLocations', methods=['get']) def getLocationsByZone(): - zone_id = int(request.args.get('id', 1)) + zone_id = int(request.args.get('zone_id', 1)) + part_id = int(request.args.get('part_id', 1)) page = int(request.args.get('page', 1)) limit = int(request.args.get('limit', 1)) @@ -248,6 +291,19 @@ def updateItemLink(): return jsonify(error=True, message="Unable to save this change, ERROR!") +@items_api.route('/item/getPossibleLocations', methods=["GET"]) +@login_required +def getPossibleLocations(): + if request.method == "GET": + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 1)) + offset = (page-1)*limit + database_config = config() + site_name = session['selected_site'] + with psycopg2.connect(**database_config) as conn: + locations, count = postsqldb.LocationsTable.paginateLocationsWithZone(conn, site_name, (limit, offset)) + return jsonify(locations=locations, end=math.ceil(count/limit)) + @items_api.route('/item/getLinkedItem', methods=["GET"]) @login_required def getLinkedItem(): @@ -471,4 +527,20 @@ def refreshSearchString(): postsqldb.ItemTable.update_tuple(conn, site_name, {'id': item_id, 'update':{'search_string': search_string}}) return jsonify(error=False, message="Search String was updated successfully") - return jsonify(error=True, message="Unable to update this search string, ERROR!") \ No newline at end of file + return jsonify(error=True, message="Unable to update this search string, ERROR!") + +@items_api.route('/item/postNewItemLocation', methods=['POST']) +def postNewItemLocation(): + if request.method == "POST": + item_id = request.get_json()['item_id'] + location_id = request.get_json()['location_id'] + database_config = config() + site_name = session['selected_site'] + with psycopg2.connect(**database_config) as conn: + item_location = postsqldb.ItemLocationsTable.Payload( + item_id, + location_id + ) + postsqldb.ItemLocationsTable.insert_tuple(conn, site_name, item_location.payload()) + return jsonify(error=False, message="Location was added successfully") + return jsonify(error=True, message="Unable to save this location, ERROR!") \ No newline at end of file diff --git a/postsqldb.py b/postsqldb.py index 62593d2..fc76581 100644 --- a/postsqldb.py +++ b/postsqldb.py @@ -383,24 +383,7 @@ class SKUPrefixTable: raise DatabaseError(error, 'PrefixTable', sql) @classmethod - def insert_tuple(self, conn, site: str, payload: list, convert=True): - record = () - with open(f"sql/INSERT/insertSKUPrefixTuple.sql", "r+") as file: - sql = file.read().replace("%%site_name%%", site) - try: - with conn.cursor() as cur: - cur.execute(sql, payload) - rows = cur.fetchone() - if rows and convert: - record = tupleDictionaryFactory(cur.description, rows) - elif rows and not convert: - record = rows - except Exception as error: - raise DatabaseError(error, payload, sql) - return record - - @classmethod - def getPrefixes(self, conn, site: str, payload: tuple, convert=True): + def paginatePrefixes(self, conn, site: str, payload: tuple, convert=True): """_summary_ Args: @@ -433,7 +416,72 @@ class SKUPrefixTable: except (Exception, psycopg2.DatabaseError) as error: raise DatabaseError(error, payload, sql) return recordset, count - + + @classmethod + def insert_tuple(self, conn, site, payload, convert=True): + """insert payload into zones table of site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + payload (tuple): (name[str],) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + prefix = () + with open(f"sql/INSERT/insertSKUPrefixTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + prefix = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + prefix = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return prefix + + @classmethod + def update_tuple(self, conn, site, payload, convert=True): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE {site}_sku_prefix SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated + class RecipesTable: @dataclass class Payload: @@ -729,8 +777,35 @@ class RecipesTable: raise DatabaseError(error, payload, sql) return updated - class ItemInfoTable: + @dataclass + class Payload: + barcode: str + packaging: str = "" + uom_quantity: float = 1.0 + uom: int = 1 + cost: float = 0.0 + safety_stock: float = 0.0 + lead_time_days: float = 0.0 + ai_pick: bool = False + prefixes: list = field(default_factory=list) + + def __post_init__(self): + if not isinstance(self.barcode, str): + raise TypeError(f"barcode must be of type str; not {type(self.barcode)}") + + def payload(self): + return ( + self.barcode, + self.packaging, + self.uom_quantity, + self.uom, + self.cost, + self.safety_stock, + self.lead_time_days, + self.ai_pick, + lst2pgarr(self.prefixes) + ) @classmethod def select_tuple(self, conn, site:str, payload:tuple, convert=True): """_summary_ @@ -795,9 +870,31 @@ class ItemInfoTable: raise DatabaseError(error, payload, sql) return updated - class ItemTable: + @classmethod + def paginateLinkedLists(self, conn, site:str, payload:tuple, convert=True): + records = [] + count = 0 + + sql = f"SELECT * FROM {site}_items WHERE row_type = 'list' LIMIT %s OFFSET %s;" + sql_count = f"SELECT COUNT(*) FROM {site}_items WHERE row_type = 'list' LIMIT %s OFFSET %s;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + records = [tupleDictionaryFactory(cur.description, row) for row in rows] + if rows and not convert: + records = rows + + cur.execute(sql_count, payload) + count = cur.fetchone()[0] + + except (Exception, psycopg2.DatabaseError) as error: + raise DatabaseError(error, payload, sql) + return records, count + @classmethod def getItemAllByID(self, conn, site, payload, convert=True): """_summary_ @@ -829,6 +926,49 @@ class ItemTable: raise DatabaseError(error, payload, getItemAllByID_sql) return item + @classmethod + def getLinkedItemByBarcode(self, conn, site, payload, convert=True): + item = () + sql = f"SELECT * FROM {site}_itemlinks WHERE barcode=%s;" + if convert: + item = {} + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + item = tupleDictionaryFactory(cur.description, rows) + if rows and not convert: + item = rows + except (Exception, psycopg2.DatabaseError) as error: + raise DatabaseError(error, payload, sql) + return item + + @classmethod + def getItemAllByBarcode(self, conn, site, payload, convert=True): + item = () + if convert: + item = {} + linked_item = self.getLinkedItemByBarcode(conn, site, (payload[0],)) + + if len(linked_item) > 1: + item = self.getItemAllByID(conn, site, payload=(linked_item['link'], ), convert=convert) + item['item_info']['uom_quantity'] = linked_item['conv_factor'] + else: + with open(f"sql/SELECT/getItemAllByBarcode.sql", "r+") as file: + getItemAllByBarcode_sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(getItemAllByBarcode_sql, payload) + rows = cur.fetchone() + if rows and convert: + item = tupleDictionaryFactory(cur.description, rows) + if rows and not convert: + item = rows + except (Exception, psycopg2.DatabaseError) as error: + raise DatabaseError(error, payload, getItemAllByBarcode_sql) + return item + @classmethod def update_tuple(self, conn, site, payload, convert=True): """_summary_ @@ -899,6 +1039,40 @@ class ReceiptTable: raise DatabaseError(error, payload, sql) return updated + @classmethod + def update_receipt_item(self, conn, site:str, payload:dict, convert=True): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE {site}_receipt_items SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated + @classmethod def select_tuple(self, conn, site:str, payload:tuple, convert=True): """_summary_ @@ -927,4 +1101,715 @@ class ReceiptTable: selected = rows except Exception as error: raise DatabaseError(error, payload, sql) - return selected \ No newline at end of file + return selected + + @classmethod + def select_item_tuple(self, conn, site:str, payload:tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + site (_type_): _description_ + payload (_type_): (receipt_id,) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + selected = () + sql = f"SELECT * FROM {site}_receipt_items WHERE id=%s;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + selected = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + selected = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return selected + +class ZonesTable: + @dataclass + class Payload: + name: str + site_id: int + description: str = "" + + def __post_init__(self): + if not isinstance(self.name, str): + raise TypeError(f"Zone name should be of type str; not {type(self.name)}") + + def payload(self): + return ( + self.name, + self.description, + self.site_id + ) + + @classmethod + def insert_tuple(self, conn, site, payload, convert=True): + """insert payload into zones table of site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + payload (tuple): (name[str],) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + zone = () + with open(f"sql/INSERT/insertZonesTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + zone = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + zone = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return zone + + @classmethod + def update_tuple(self, conn, site, payload, convert=True): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE {site}_zones SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated + + @classmethod + def paginateZones(self, conn, site:str, payload:tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + site (str): _description_ + payload (tuple): (limit, offset) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + recordset = () + count = 0 + sql = f"SELECT * FROM {site}_zones LIMIT %s OFFSET %s;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + recordset = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + recordset = rows + + cur.execute(f"SELECT COUNT(*) FROM {site}_zones;") + count = cur.fetchone()[0] + except Exception as error: + raise DatabaseError(error, (), sql) + return recordset, count + + @classmethod + def paginateZonesBySku(self, conn, site: str, payload: tuple, convert=True): + zones = () + count = 0 + with open(f"sql/SELECT/zones/paginateZonesBySku.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + with open(f"sql/SELECT/zones/paginateZonesBySkuCount.sql", "r+") as file: + sql_count = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + zones = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + zones = rows + + cur.execute(sql_count, payload) + count = cur.fetchone()[0] + + except Exception as error: + raise DatabaseError(error, payload, sql) + return zones, count + +class LocationsTable: + @dataclass + class Payload: + uuid: str + name: str + zone_id: int + + def __post_init__(self): + if not isinstance(self.uuid, str): + raise TypeError(f"uuid must be of type str; not {type(self.uuid)}") + if not isinstance(self.name, str): + raise TypeError(f"Location name must be of type str; not {type(self.name)}") + if not isinstance(self.zone_id, int): + raise TypeError(f"zone_id must be of type str; not {type(self.zone_id)}") + + def payload(self): + return ( + self.uuid, + self.name, + self.zone_id + ) + + @classmethod + def paginateLocations(self, conn, site:str, payload:tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + site (str): _description_ + payload (tuple): (limit, offset) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + recordset = () + count = 0 + sql = f"SELECT * FROM {site}_locations LIMIT %s OFFSET %s;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + recordset = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + recordset = rows + + cur.execute(f"SELECT COUNT(*) FROM {site}_locations;") + count = cur.fetchone()[0] + except Exception as error: + raise DatabaseError(error, (), sql) + return recordset, count + + @classmethod + def paginateLocationsWithZone(self, conn, site:str, payload:tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + site (str): _description_ + payload (tuple): (limit, offset) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + recordset = () + count = 0 + with open(f"sql/SELECT/getLocationsWithZone.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + recordset = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + recordset = rows + cur.execute(f"SELECT COUNT(*) FROM {site}_locations;") + count = cur.fetchone()[0] + except Exception as error: + raise DatabaseError(error, (), sql) + return recordset, count + + @classmethod + def paginateLocationsBySkuZone(self, conn, site: str, payload: tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + site (str): _description_ + payload (tuple): (item_id, zone_id, limit, offset) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + locations = () + count = 0 + with open(f"sql/SELECT/locations/paginateLocationsBySkuZone.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + with open(f"sql/SELECT/locations/paginateLocationsBySkuZoneCount.sql", "r+") as file: + sql_count = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + locations = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + locations = rows + + cur.execute(sql_count, payload) + count = cur.fetchone()[0] + + except Exception as error: + raise DatabaseError(error, payload, sql) + return locations, count + + @classmethod + def insert_tuple(self, conn, site, payload, convert=True): + """insert payload into zones table of site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + payload (tuple): (name[str],) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + zone = () + with open(f"sql/INSERT/insertLocationsTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + zone = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + zone = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return zone + +class VendorsTable: + @dataclass + class Payload: + vendor_name: str + created_by: int + vendor_address: str = "" + creation_date: datetime.datetime = field(init=False) + phone_number: str = "" + + def __post_init__(self): + if not isinstance(self.vendor_name, str): + raise TypeError(f"vendor_name should be of type str; not {type(self.vendor_name)}") + self.creation_date = datetime.datetime.now() + + + def payload(self): + return ( + self.vendor_name, + self.vendor_address, + self.creation_date, + self.created_by, + self.phone_number + ) + + @classmethod + def paginateVendors(self, conn, site:str, payload:tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + site (str): _description_ + payload (tuple): (limit, offset) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + recordset = () + count = 0 + sql = f"SELECT * FROM {site}_vendors LIMIT %s OFFSET %s;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + recordset = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + recordset = rows + + cur.execute(f"SELECT COUNT(*) FROM {site}_vendors;") + count = cur.fetchone()[0] + except Exception as error: + raise DatabaseError(error, (), sql) + return recordset, count + + @classmethod + def insert_tuple(self, conn, site, payload, convert=True): + """insert payload into zones table of site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + payload (tuple): (name[str],) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + zone = () + with open(f"sql/INSERT/insertVendorsTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + zone = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + zone = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return zone + + @classmethod + def update_tuple(self, conn, site, payload, convert=True): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE {site}_vendors SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated + +class BrandsTable: + @dataclass + class Payload: + name: str + + def __post_init__(self): + if not isinstance(self.name, str): + return TypeError(f"brand name should be of type str; not {type(self.name)}") + + def payload(self): + return ( + self.name, + ) + + @classmethod + def paginateBrands(self, conn, site:str, payload:tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + site (str): _description_ + payload (tuple): (limit, offset) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + recordset = () + count = 0 + sql = f"SELECT * FROM {site}_brands LIMIT %s OFFSET %s;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + recordset = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + recordset = rows + + cur.execute(f"SELECT COUNT(*) FROM {site}_brands;") + count = cur.fetchone()[0] + except Exception as error: + raise DatabaseError(error, (), sql) + return recordset, count + + @classmethod + def insert_tuple(self, conn, site, payload, convert=True): + """insert payload into zones table of site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + payload (tuple): (name[str],) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + brand = () + with open(f"sql/INSERT/insertBrandsTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + brand = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + brand = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return brand + + @classmethod + def update_tuple(self, conn, site, payload, convert=True): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE {site}_brands SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated + +class ItemLocationsTable: + @dataclass + class Payload: + part_id: int + location_id: int + quantity_on_hand: float = 0.0 + cost_layers: list = field(default_factory=list) + + def __post_init__(self): + if not isinstance(self.part_id, int): + raise TypeError(f"part_id must be of type int; not {type(self.part_id)}") + if not isinstance(self.location_id, int): + raise TypeError(f"part_id must be of type int; not {type(self.part_id)}") + + def payload(self): + return ( + self.part_id, + self.location_id, + self.quantity_on_hand, + lst2pgarr(self.cost_layers) + ) + + @classmethod + def insert_tuple(self, conn, site, payload, convert=True): + """insert payload into zones table of site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + payload (tuple): (name[str],) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + item_location = () + with open(f"sql/INSERT/insertItemLocationsTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + item_location = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + item_location = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return item_location + + @classmethod + def select_by_id(self, conn, site: str, payload: tuple, convert=True): + item_locations = () + with open(f"sql/SELECT/selectItemLocations", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + item_locations = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + item_locations = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return item_locations + +class ItemLinksTable: + @dataclass + class Payload: + barcode: str + link: int + data: dict = field(default_factory=dict) + conv_factor: float = 1 + + def __post_init__(self): + if not isinstance(self.barcode, str): + raise TypeError(f"barcode must be of type str; not {type(self.barocde)}") + if not isinstance(self.link, int): + raise TypeError(f"link must be of type str; not {type(self.link)}") + + def payload(self): + return ( + self.barcode, + self.link, + json.dumps(self.data), + self.conv_factor + ) + + @classmethod + def insert_tuple(self, conn, site:str, payload:tuple, convert=True): + """insert payload into itemlinks table of site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + payload (tuple): (barcode[str], link[int], data[jsonb], conv_factor[float]) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + link = () + with open(f"sql/INSERT/insertItemLinksTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + link = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + link = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return link + +class CycleCountsTable: + @dataclass + class Payload: + pass + +class SitesTable: + @classmethod + def selectTuples(self, conn, convert=True): + recordsets = [] + sql = f"SELECT * FROM sites" + try: + with conn.cursor() as cur: + cur.execute(sql) + rows = cur.fetchall() + if rows and convert: + recordsets = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + recordsets = rows + except Exception as error: + raise DatabaseError(error, (), sql) + return recordsets \ No newline at end of file diff --git a/process.py b/process.py index c691758..e876eda 100644 --- a/process.py +++ b/process.py @@ -1,5 +1,6 @@ import database, MyDataclasses, psycopg2, datetime,json from config import config +import postsqldb def dropSiteTables(conn, site_manager: MyDataclasses.SiteManager): try: @@ -148,7 +149,7 @@ def postNewBlankItem(conn, site_name: str, user_id: int, data: dict): ) # create item info - item_info = MyDataclasses.ItemInfoPayload(data['barcode']) + item_info = postsqldb.ItemInfoTable.Payload(data['barcode']) # create Food Info food_info = MyDataclasses.FoodInfoPayload() @@ -190,7 +191,7 @@ def postNewBlankItem(conn, site_name: str, user_id: int, data: dict): location_id = cur.fetchone()[0] - item_location = MyDataclasses.ItemLocationPayload(item['id'], location_id) + item_location = postsqldb.ItemLocationsTable.Payload(item['id'], location_id) database.insertItemLocationsTuple(conn, site_name, item_location.payload()) diff --git a/receipts_API.py b/receipts_API.py index e426a5d..a9a6d56 100644 --- a/receipts_API.py +++ b/receipts_API.py @@ -6,6 +6,7 @@ import openfoodfacts import postsqldb import mimetypes, os import pymupdf, PIL +import webpush def create_pdf_preview(pdf_path, output_path, size=(600, 400)): @@ -53,6 +54,38 @@ def getItems(): return jsonify({"items":recordset, "end":math.ceil(count['count']/limit), "error":False, "message":"items fetched succesfully!"}) return jsonify({"items":recordset, "end":math.ceil(count['count']/limit), "error":True, "message":"There was an error with this GET statement"}) +@receipt_api.route('/receipt/getVendors', methods=["GET"]) +def getVendors(): + recordset = [] + count = 0 + if request.method == "GET": + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 10)) + site_name = session['selected_site'] + offset = (page - 1) * limit + database_config = config() + with psycopg2.connect(**database_config) as conn: + payload = (limit, offset) + recordset, count = postsqldb.VendorsTable.paginateVendors(conn, site_name, payload) + return jsonify({"vendors":recordset, "end":math.ceil(count/limit), "error":False, "message":"items fetched succesfully!"}) + return jsonify({"vendors":recordset, "end":math.ceil(count/limit), "error":True, "message":"There was an error with this GET statement"}) + +@receipt_api.route('/receipt/getLinkedLists', methods=["GET"]) +def getLinkedLists(): + recordset = [] + count = 0 + if request.method == "GET": + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 10)) + site_name = session['selected_site'] + offset = (page - 1) * limit + database_config = config() + with psycopg2.connect(**database_config) as conn: + payload = (limit, offset) + recordset, count = postsqldb.ItemTable.paginateLinkedLists(conn, site_name, payload) + return jsonify({"items":recordset, "end":math.ceil(count/limit), "error":False, "message":"items fetched succesfully!"}) + return jsonify({"items":recordset, "end":math.ceil(count/limit), "error":True, "message":"There was an error with this GET statement"}) + @receipt_api.route('/receipts/getReceipts', methods=["GET"]) def getReceipts(): recordset = [] @@ -158,7 +191,65 @@ def saveLine(): database.__updateTuple(conn, site_name, f"{site_name}_receipt_items", {'id': line_id, 'update': payload}) return jsonify({'error': False, "message": "Line Saved Succesfully"}) return jsonify({'error': True, "message": "Something went wrong while saving line!"}) - + +@receipt_api.route('/receipt/postLinkedItem', methods=["POST"]) +def postLinkedItem(): + if request.method == "POST": + receipt_item_id = int(request.get_json()['receipt_item_id']) + link_list_id = int(request.get_json()['link_list_id']) + conv_factor = float(request.get_json()['conv_factor']) + + site_name = session['selected_site'] + user_id = session['user_id'] + database_config = config() + with psycopg2.connect(**database_config) as conn: + receipt_item = postsqldb.ReceiptTable.select_item_tuple(conn, site_name, (receipt_item_id,)) + # get link list item + linked_list = postsqldb.ItemTable.getItemAllByID(conn, site_name, (link_list_id, )) + # add item to database + if receipt_item['type'] == 'api': + + data = { + 'barcode': receipt_item['barcode'], + 'name': receipt_item['name'], + 'subtype': 'FOOD' + } + process.postNewBlankItem(conn, site_name, user_id, data) + + name = receipt_item['name'] + if receipt_item['name'] == "unknown": + name = linked_list['item_name'] + if receipt_item['type'] == "new sku": + data = { + 'barcode': receipt_item['barcode'], + 'name': name, + 'subtype': 'FOOD' + } + process.postNewBlankItem(conn, site_name, user_id, data) + + new_item = postsqldb.ItemTable.getItemAllByBarcode(conn, site_name, (receipt_item['barcode'], )) + new_item = postsqldb.ItemTable.update_tuple(conn, site_name, {'id': new_item['id'], 'update':{'row_type': 'link'}}) + + # add item to link list + item_link = postsqldb.ItemLinksTable.Payload( + new_item['barcode'], + linked_list['id'], + new_item, + conv_factor + ) + postsqldb.ItemLinksTable.insert_tuple(conn, site_name, item_link.payload()) + # update line item with link list name and item_link with link list id + payload = {'id': receipt_item['id'], 'update': { + 'barcode': linked_list['barcode'], + 'name': linked_list['item_name'], + 'uom': linked_list['item_info']['uom']['id'], + 'qty': float(receipt_item['qty']*conv_factor), + 'type': 'sku' + }} + postsqldb.ReceiptTable.update_receipt_item(conn, site_name, payload) + + return jsonify({'error': False, "message": "Line Saved Succesfully"}) + return jsonify({'error': True, "message": "Something went wrong while saving line!"}) @receipt_api.route('/receipts/resolveLine', methods=["POST"]) def resolveLine(): @@ -248,14 +339,28 @@ def resolveLine(): return jsonify({'error': False, "message": "Line Saved Succesfully"}) return jsonify({'error': True, "message": "Something went wrong while saving line!"}) +@receipt_api.route('/receipt/postVendorUpdate', methods=["POST"]) +def postVendorUpdate(): + if request.method == "POST": + receipt_id = int(request.get_json()['receipt_id']) + vendor_id = int(request.get_json()['vendor_id']) + site_name = session['selected_site'] + database_config = config() + with psycopg2.connect(**database_config) as conn: + postsqldb.ReceiptTable.update_receipt(conn, site_name, {'id': receipt_id, 'update': {'vendor_id': vendor_id}}) + return jsonify({'error': False, "message": "Line Saved Succesfully"}) + return jsonify({'error': True, "message": "Something went wrong while saving line!"}) + @receipt_api.route('/receipts/resolveReceipt', methods=["POST"]) def resolveReceipt(): if request.method == "POST": receipt_id = int(request.get_json()['receipt_id']) site_name = session['selected_site'] + user= session['user'] database_config = config() with psycopg2.connect(**database_config) as conn: - postsqldb.ReceiptTable.update_receipt(conn, site_name, {'id': receipt_id, 'update': {'receipt_status': 'Resolved'}}) + receipt = postsqldb.ReceiptTable.update_receipt(conn, site_name, {'id': receipt_id, 'update': {'receipt_status': 'Resolved'}}) + webpush.push_ntfy(title=f"Receipt '{receipt['receipt_id']}' Resolved", body=f"Receipt {receipt['receipt_id']} was completed by {user['username']}.") return jsonify({'error': False, "message": "Line Saved Succesfully"}) return jsonify({'error': True, "message": "Something went wrong while saving line!"}) diff --git a/recipes_api.py b/recipes_api.py index b6b90e9..064344e 100644 --- a/recipes_api.py +++ b/recipes_api.py @@ -4,7 +4,7 @@ from config import config, sites_config from main import unfoldCostLayers from user_api import login_required import os -import postsqldb +import postsqldb, webpush recipes_api = Blueprint('recipes_api', __name__) @@ -19,7 +19,6 @@ def recipes(): @recipes_api.route("/recipe//") @login_required def recipe(mode, id): - database_config = config() with psycopg2.connect(**database_config) as conn: units = postsqldb.UnitsTable.getAll(conn) @@ -68,7 +67,8 @@ def addRecipe(): author=user_id, description=recipe_description ) - postsqldb.RecipesTable.insert_tuple(conn, site_name, recipe.payload()) + recipe = postsqldb.RecipesTable.insert_tuple(conn, site_name, recipe.payload()) + webpush.push_ntfy('New Recipe', f"New Recipe added to {site_name}; {recipe_name}!{recipe_description} http://test.treehousefullofstars.com/recipe/view/{recipe['id']} http://test.treehousefullofstars.com/recipe/edit/{recipe['id']}") return jsonify({'recipe': recipe, 'error': False, 'message': 'Add Recipe successful!'}) return jsonify({'recipe': recipe, 'error': True, 'message': 'Add Recipe unsuccessful!'}) diff --git a/sql/CREATE/zones.sql b/sql/CREATE/zones.sql index 03f49c2..5da4e93 100644 --- a/sql/CREATE/zones.sql +++ b/sql/CREATE/zones.sql @@ -1,6 +1,7 @@ CREATE TABLE IF NOT EXISTS %%site_name%%_zones( id SERIAL PRIMARY KEY, name VARCHAR(32) NOT NULL, + description TEXT, site_id INTEGER NOT NULL, UNIQUE(name), CONSTRAINT fk_site diff --git a/sql/INSERT/insertZonesTuple.sql b/sql/INSERT/insertZonesTuple.sql index b551143..d109e77 100644 --- a/sql/INSERT/insertZonesTuple.sql +++ b/sql/INSERT/insertZonesTuple.sql @@ -1,4 +1,4 @@ INSERT INTO %%site_name%%_zones -(name, site_id) -VALUES (%s, %s) +(name, description, site_id) +VALUES (%s, %s, %s) RETURNING *; \ No newline at end of file diff --git a/sql/SELECT/getItemsWithQOH.sql b/sql/SELECT/getItemsWithQOH.sql index a1391d7..fb1b171 100644 --- a/sql/SELECT/getItemsWithQOH.sql +++ b/sql/SELECT/getItemsWithQOH.sql @@ -13,6 +13,6 @@ FROM %%site_name%%_items LEFT JOIN sum_cte ON %%site_name%%_items.id = sum_cte.id LEFT JOIN %%site_name%%_item_info ON %%site_name%%_items.item_info_id = %%site_name%%_item_info.id WHERE %%site_name%%_items.search_string LIKE '%%' || %s || '%%' -ORDER BY %%site_name%%_items.item_name ASC +ORDER BY %%sort_order%% LIMIT %s OFFSET %s; diff --git a/sql/SELECT/getLocationsWithZone.sql b/sql/SELECT/getLocationsWithZone.sql new file mode 100644 index 0000000..9584475 --- /dev/null +++ b/sql/SELECT/getLocationsWithZone.sql @@ -0,0 +1,5 @@ +SELECT %%site_name%%_locations.*, +row_to_json(%%site_name%%_zones.*) as zone +FROM %%site_name%%_locations +LEFT JOIN %%site_name%%_zones ON %%site_name%%_zones.id = %%site_name%%_locations.zone_id +LIMIT %s OFFSET %s; \ No newline at end of file diff --git a/sql/SELECT/locations/paginateLocationsBySkuZone.sql b/sql/SELECT/locations/paginateLocationsBySkuZone.sql new file mode 100644 index 0000000..6574408 --- /dev/null +++ b/sql/SELECT/locations/paginateLocationsBySkuZone.sql @@ -0,0 +1,15 @@ +WITH passed_id AS (SELECT %s AS passed_id), + cte_item_locations AS ( + SELECT DISTINCT ils.location_id FROM %%site_name%%_item_locations ils + WHERE ils.part_id = (SELECT passed_id FROM passed_id) + ), + cte_locations AS ( + SELECT DISTINCT locations.zone_id FROM %%site_name%%_locations locations + WHERE locations.id IN (SELECT location_id FROM cte_item_locations) + ) + + +SELECT DISTINCT location.* FROM cte_item_locations cil +JOIN %%site_name%%_locations location ON cil.location_id = location.id +WHERE location.zone_id = %s +LIMIT %s OFFSET %s; diff --git a/sql/SELECT/locations/paginateLocationsBySkuZoneCount.sql b/sql/SELECT/locations/paginateLocationsBySkuZoneCount.sql new file mode 100644 index 0000000..9a2fb9d --- /dev/null +++ b/sql/SELECT/locations/paginateLocationsBySkuZoneCount.sql @@ -0,0 +1,15 @@ +WITH passed_id AS (SELECT %s AS passed_id), + cte_item_locations AS ( + SELECT DISTINCT ils.location_id FROM %%site_name%%_item_locations ils + WHERE ils.part_id = (SELECT passed_id FROM passed_id) + ), + cte_locations AS ( + SELECT DISTINCT locations.zone_id FROM %%site_name%%_locations locations + WHERE locations.id IN (SELECT location_id FROM cte_item_locations) + ) + + +SELECT COUNT(DISTINCT location.*) FROM cte_item_locations cil +JOIN %%site_name%%_locations location ON cil.location_id = location.id +WHERE location.zone_id = %s +LIMIT %s OFFSET %s; diff --git a/sql/SELECT/selectItemLocations.sql b/sql/SELECT/selectItemLocations.sql new file mode 100644 index 0000000..8ba86ea --- /dev/null +++ b/sql/SELECT/selectItemLocations.sql @@ -0,0 +1,3 @@ +SELECT il.* FROM %%site_name%%_item_locations il +LEFT JOIN %%site_name%%_zones zone ON zone.id = il.zone_id +WHERE il.id = %s; \ No newline at end of file diff --git a/sql/SELECT/zones/paginateZonesBySku.sql b/sql/SELECT/zones/paginateZonesBySku.sql new file mode 100644 index 0000000..16f8cb3 --- /dev/null +++ b/sql/SELECT/zones/paginateZonesBySku.sql @@ -0,0 +1,15 @@ +WITH passed_id AS (SELECT %s AS passed_id), + cte_item_locations AS ( + SELECT DISTINCT ils.location_id FROM %%site_name%%_item_locations ils + WHERE ils.part_id = (SELECT passed_id FROM passed_id) + ), + cte_locations AS ( + SELECT DISTINCT locations.zone_id FROM %%site_name%%_locations locations + WHERE locations.id IN (SELECT location_id FROM cte_item_locations) + ) + + +SELECT DISTINCT zone.* FROM cte_locations cil +JOIN %%site_name%%_zones zone ON cil.zone_id = zone.id +LIMIT %s OFFSET %s; + diff --git a/sql/SELECT/zones/paginateZonesBySkuCount.sql b/sql/SELECT/zones/paginateZonesBySkuCount.sql new file mode 100644 index 0000000..4174fba --- /dev/null +++ b/sql/SELECT/zones/paginateZonesBySkuCount.sql @@ -0,0 +1,13 @@ +WITH passed_id AS (SELECT %s AS passed_id), + cte_item_locations AS ( + SELECT DISTINCT ils.location_id FROM %%site_name%%_item_locations ils + WHERE ils.part_id = (SELECT passed_id FROM passed_id) + ), + cte_locations AS ( + SELECT DISTINCT locations.zone_id FROM %%site_name%%_locations locations + WHERE locations.id IN (SELECT location_id FROM cte_item_locations) + ) + +SELECT COUNT(DISTINCT zone.*) FROM cte_locations cil +JOIN %%site_name%%_zones zone ON cil.zone_id = zone.id +LIMIT %s OFFSET %s; diff --git a/static/css/pantry.css b/static/css/pantry.css index 3fc0d29..ec7fc00 100644 --- a/static/css/pantry.css +++ b/static/css/pantry.css @@ -83,4 +83,19 @@ .instruction-list{ list-style-type: none; padding: 0; +} + +.add-button{ + width: 100%; + border-radius: 10px; + margin-top: 10px; + background-color: transparent; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + height: 40px; +} +.add-button:hover{ + background-color: whitesmoke; } \ No newline at end of file diff --git a/static/files/receipts/20250417_185950.jpg b/static/files/receipts/20250417_185950.jpg new file mode 100644 index 0000000..36cd758 Binary files /dev/null and b/static/files/receipts/20250417_185950.jpg differ diff --git a/static/files/receipts/Order_details_-_Walmart.com_04182025.pdf b/static/files/receipts/Order_details_-_Walmart.com_04182025.pdf new file mode 100644 index 0000000..9954360 Binary files /dev/null and b/static/files/receipts/Order_details_-_Walmart.com_04182025.pdf differ diff --git a/static/files/receipts/previews/Order_details_-_Walmart.com_04182025.jpg b/static/files/receipts/previews/Order_details_-_Walmart.com_04182025.jpg new file mode 100644 index 0000000..953ca61 Binary files /dev/null and b/static/files/receipts/previews/Order_details_-_Walmart.com_04182025.jpg differ diff --git a/static/handlers/ItemListHandler.js b/static/handlers/ItemListHandler.js index cb65828..8e40e98 100644 --- a/static/handlers/ItemListHandler.js +++ b/static/handlers/ItemListHandler.js @@ -185,7 +185,7 @@ async function updateTableElements(){ let opsCell = document.createElement('th') opsCell.innerHTML = 'Operations' - head_row.append(nameCell, descriptionCell, opsCell) + head_row.append(nameCell, descriptionCell, qtyUOMCell, opsCell) table_head.append(head_row) main_table.append(table_head) @@ -281,12 +281,27 @@ async function updateListElements(){ items_list.append(main_list) } +let sort = "id" +async function setSort(sort_string) { + sort = sort_string + await getItems() + await reloadCards() +} + +let order = "ASC" +async function setOrder(order_string) { + order = order_string + await getItems() + await reloadCards() +} async function getItems(){ + const url = new URL('/item/getItemsWithQOH', window.location.origin); url.searchParams.append('page', current_page); url.searchParams.append('limit', limit); url.searchParams.append('search_text', searchText); - url.searchParams.append('sort_order', sort_order); + url.searchParams.append('sort', sort); + url.searchParams.append('order', order); url.searchParams.append('view', view); await fetch(url) diff --git a/static/handlers/itemEditHandler.js b/static/handlers/itemEditHandler.js index 392dc72..e7577d6 100644 --- a/static/handlers/itemEditHandler.js +++ b/static/handlers/itemEditHandler.js @@ -1,3 +1,34 @@ +var darkmode = false +function toggleDarkMode(){ + if (!darkmode){ + document.body.classList.add('dark-mode-body') + document.body.classList.add('uk-light') + document.getElementById('navbar').classList.add('uk-light') + document.getElementById('navbar').style = "background-color: #121212;" + document.getElementById('weblinkModal').classList.add('dark-mode-element') + document.getElementById('weblinkModalFooter').classList.add('dark-mode-element') + document.getElementById('brandsModalinner').classList.add('dark-mode-element') + document.getElementById('locationsModalInner').classList.add('dark-mode-element') + document.getElementById('zonesModalInner').classList.add('dark-mode-element') + document.getElementById('modeToggle').innerHTML = "light_mode" + + darkmode = true + } else { + document.body.classList.remove('dark-mode-body') + document.body.classList.remove('uk-light') + document.getElementById('navbar').classList.remove('uk-light') + document.getElementById('navbar').style = "" + document.getElementById('weblinkModal').classList.remove('dark-mode-element') + document.getElementById('weblinkModalFooter').classList.remove('dark-mode-element') + document.getElementById('brandsModalinner').classList.remove('dark-mode-element') + document.getElementById('locationsModalInner').classList.remove('dark-mode-element') + document.getElementById('zonesModalInner').classList.remove('dark-mode-element') + document.getElementById('modeToggle').innerHTML = "dark_mode" + + darkmode=false + } +} + var item; var linked_items; var tags = new Set(); @@ -920,9 +951,10 @@ async function fetchItems() { let zones_limit = 20; async function fetchZones(){ - const url = new URL('/item/getZones', window.location.origin); + const url = new URL('/item/getZonesBySku', window.location.origin); url.searchParams.append('page', current_page); url.searchParams.append('limit', zones_limit); + url.searchParams.append('item_id', item.id); const response = await fetch(url); data = await response.json(); return data; @@ -930,13 +962,14 @@ async function fetchZones(){ let locations_limit = 10; async function fetchLocations(logis) { - const url = new URL('/item/getLocations', window.location.origin); + const url = new URL('/item/getLocationsBySkuZone', window.location.origin); url.searchParams.append('page', current_page); url.searchParams.append('limit', locations_limit); + url.searchParams.append('part_id', item.id); if(logis=="primary_location"){ - url.searchParams.append('id', primary_zone_id); + url.searchParams.append('zone_id', primary_zone_id); } else if (logis=="auto_issue_location"){ - url.searchParams.append('id', auto_zone_id); + url.searchParams.append('zone_id', auto_zone_id); } const response = await fetch(url); data = await response.json(); @@ -1332,33 +1365,156 @@ async function updatePrefixPaginationElement() { paginationElement.append(nextElement) } -var darkmode = false -function toggleDarkMode(){ - if (!darkmode){ - document.body.classList.add('dark-mode-body') - document.body.classList.add('uk-light') - document.getElementById('navbar').classList.add('uk-light') - document.getElementById('navbar').style = "background-color: #121212;" - document.getElementById('weblinkModal').classList.add('dark-mode-element') - document.getElementById('weblinkModalFooter').classList.add('dark-mode-element') - document.getElementById('brandsModalinner').classList.add('dark-mode-element') - document.getElementById('locationsModalInner').classList.add('dark-mode-element') - document.getElementById('zonesModalInner').classList.add('dark-mode-element') - document.getElementById('modeToggle').innerHTML = "light_mode" +// Possible Locations functions +var new_locations_current_page = 1 +var new_locations_end_page = 1 +var new_locations_limit = 25 +async function fetch_new_locations() { + const url = new URL('/item/getPossibleLocations', window.location.origin); + url.searchParams.append('page', new_locations_current_page); + url.searchParams.append('limit', new_locations_limit); + const response = await fetch(url); + data = await response.json(); + new_locations_end_page = data.end; + return data.locations +}; - darkmode = true - } else { - document.body.classList.remove('dark-mode-body') - document.body.classList.remove('uk-light') - document.getElementById('navbar').classList.remove('uk-light') - document.getElementById('navbar').style = "" - document.getElementById('weblinkModal').classList.remove('dark-mode-element') - document.getElementById('weblinkModalFooter').classList.remove('dark-mode-element') - document.getElementById('brandsModalinner').classList.remove('dark-mode-element') - document.getElementById('locationsModalInner').classList.remove('dark-mode-element') - document.getElementById('zonesModalInner').classList.remove('dark-mode-element') - document.getElementById('modeToggle').innerHTML = "dark_mode" +async function postNewItemLocation(location_id) { + const response = await fetch(`/item/postNewItemLocation`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + item_id: parseInt(item_id), + location_id: parseInt(location_id) + }), + }); - darkmode=false + data = await response.json(); + response_status = 'success' + if (data.error){ + response_status = 'danger' } + + UIkit.notification({ + message: data.message, + status: response_status, + pos: 'top-right', + timeout: 5000 + }); + await fetchItem() + await updateLocationsTable() +} + +async function replenishPossibleLocationsTableBody(locations){ + let NewLocationsModalTableBody = document.getElementById('NewLocationsModalTableBody') + NewLocationsModalTableBody.innerHTML = "" + + for(let i =0; i `; + previousElement.classList.add('uk-disabled'); + }else { + previousElement.innerHTML = ``; + } + paginationElement.append(previousElement) + + //first + let firstElement = document.createElement('li') + if(new_locations_current_page<=1){ + firstElement.innerHTML = `1`; + firstElement.classList.add('uk-disabled'); + }else { + firstElement.innerHTML = `1`; + } + paginationElement.append(firstElement) + + // ... + if(new_locations_current_page-2>1){ + let firstDotElement = document.createElement('li') + firstDotElement.classList.add('uk-disabled') + firstDotElement.innerHTML = ``; + paginationElement.append(firstDotElement) + } + // last + if(new_locations_current_page-2>0){ + let lastElement = document.createElement('li') + lastElement.innerHTML = `${new_locations_current_page-1}` + paginationElement.append(lastElement) + } + // current + if(new_locations_current_page!=1 && new_locations_current_page != new_locations_end_page){ + let currentElement = document.createElement('li') + currentElement.innerHTML = `
  • ${new_locations_current_page}
  • ` + paginationElement.append(currentElement) + } + // next + if(new_locations_current_page+2${new_locations_current_page+1}` + paginationElement.append(nextElement) + } + // ... + if(new_locations_current_page+2<=new_locations_end_page){ + let secondDotElement = document.createElement('li') + secondDotElement.classList.add('uk-disabled') + secondDotElement.innerHTML = ``; + paginationElement.append(secondDotElement) + } + //end + let endElement = document.createElement('li') + if(new_locations_current_page>=new_locations_end_page){ + endElement.innerHTML = `${new_locations_end_page}`; + endElement.classList.add('uk-disabled'); + }else { + endElement.innerHTML = `${new_locations_end_page}`; + } + paginationElement.append(endElement) + //next button + let nextElement = document.createElement('li') + if(new_locations_current_page>=new_locations_end_page){ + nextElement.innerHTML = ``; + nextElement.classList.add('uk-disabled'); + }else { + nextElement.innerHTML = ``; + } + paginationElement.append(nextElement) } \ No newline at end of file diff --git a/static/handlers/loginHandler.js b/static/handlers/loginHandler.js index 000d638..99e8216 100644 --- a/static/handlers/loginHandler.js +++ b/static/handlers/loginHandler.js @@ -1,3 +1,9 @@ +async function passwordEnter(event) { + if(event.key == "Enter"){ + await loginUser() + } +} + async function loginUser() { let username = document.getElementById('login_username').value let password = document.getElementById('login_password').value diff --git a/static/handlers/receiptHandler.js b/static/handlers/receiptHandler.js index a12216d..993fbb3 100644 --- a/static/handlers/receiptHandler.js +++ b/static/handlers/receiptHandler.js @@ -7,7 +7,6 @@ document.addEventListener('DOMContentLoaded', async function() { async function refreshReceipt() { let receipt = await getReceipt(receipt_id) - console.log(receipt) await replenishFields(receipt) await replenishLinesTable(receipt.receipt_items) await replenishFilesCards(receipt.files) @@ -26,6 +25,14 @@ async function replenishFields(receipt) { document.getElementById('vendor_name').value = receipt.vendor.vendor_name document.getElementById('vendor_address').value = receipt.vendor.vendor_address document.getElementById('vendor_phone').value = receipt.vendor.phone_number + if(receipt.receipt_status=="Resolved"){ + document.getElementById('resolveReceiptButton').hidden = true + document.getElementById('lineAddButton').hidden = true + document.getElementById('fileUploadButton').hidden = true + document.getElementById('fileUploadForm').hidden = true + document.getElementById('vendorSelectDiv').hidden = true + document.getElementById('vendorSelectButton').hidden = true + } } } @@ -105,6 +112,14 @@ async function replenishLinesTable(receipt_items) { } } + let linkOp = document.createElement('a') + linkOp.style = "margin-right: 5px;" + linkOp.setAttribute('class', 'uk-button uk-button-small uk-button-default') + linkOp.setAttribute('uk-icon', 'icon: link') + linkOp.onclick = async function () { + await openLinksSelectModal(receipt_items[i].id) + } + let editOp = document.createElement('a') editOp.style = "margin-right: 5px;" editOp.setAttribute('class', 'uk-button uk-button-small uk-button-default') @@ -139,6 +154,11 @@ async function replenishLinesTable(receipt_items) { if (receipt_items[i].type === "new sku"){ operationsCell.append(apiOp) + operationsCell.append(linkOp) + } + + if (receipt_items[i].type === "api"){ + operationsCell.append(linkOp) } operationsCell.append(editOp, resolveOp, denyOp, deleteOp) @@ -505,3 +525,342 @@ async function updateItemsPaginationElement() { } paginationElement.append(nextElement) } + +// Select Vedor functions +let vendor_limit = 25 +let vendor_current_page = 1 +let vendor_end_page = 10 +async function getVendors() { + const url = new URL('/receipt/getVendors', window.location.origin); + url.searchParams.append('page', vendor_current_page); + url.searchParams.append('limit', vendor_limit); + const response = await fetch(url); + data = await response.json(); + vendor_end_page = data.end + return data.vendors; +} + +async function postVendorUpdate(vendor_id) { + const response = await fetch(`/receipt/postVendorUpdate`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + receipt_id: receipt_id, + vendor_id: vendor_id + }), + }); + data = await response.json() + message_type = "primary" + if(data.error){ + message_type = "danger" + } + UIkit.notification({ + message: data.message, + status: message_type, + pos: 'top-right', + timeout: 5000 + }); + await refreshReceipt() + UIkit.modal(document.getElementById("vendorsModal")).hide(); + +} + +async function openVendorsSelectModal() { + let vendors = await getVendors(); + await replenishVendorsTableBody(vendors); + await updateVendorsPaginationElement() + UIkit.modal(document.getElementById("vendorsModal")).show(); +} + +async function replenishVendorsTableBody(vendors) { + let vendorsTableBody = document.getElementById('vendorsTableBody') + vendorsTableBody.innerHTML = "" + + for(let i=0; i < vendors.length; i++){ + let tableRow = document.createElement('tr') + + let idCell = document.createElement('td') + idCell.innerHTML = vendors[i].id + + let nameCell = document.createElement('td') + nameCell.innerHTML = vendors[i].vendor_name + + let phoneCell = document.createElement('td') + phoneCell.innerHTML = vendors[i].phone_number + + let addressCell = document.createElement('td') + addressCell.innerHTML = vendors[i].vendor_address + + tableRow.onclick = async function() { + await postVendorUpdate(vendors[i].id) + } + + tableRow.append(idCell,nameCell,phoneCell, addressCell) + vendorsTableBody.append(tableRow) + } + +} + +async function setVendorPage(pageNumber) { + vendor_current_page = pageNumber; + let vendors = await getVendors() + await updateVendorsPaginationElement() + await replenishVendorsTableBody(vendors) +} + +async function updateVendorsPaginationElement() { + let paginationElement = document.getElementById("vendorsPage"); + paginationElement.innerHTML = ""; + // previous + let previousElement = document.createElement('li') + if(vendor_current_page<=1){ + previousElement.innerHTML = ``; + previousElement.classList.add('uk-disabled'); + }else { + previousElement.innerHTML = ``; + } + paginationElement.append(previousElement) + + //first + let firstElement = document.createElement('li') + if(vendor_current_page<=1){ + firstElement.innerHTML = `1`; + firstElement.classList.add('uk-disabled'); + }else { + firstElement.innerHTML = `1`; + } + paginationElement.append(firstElement) + + // ... + if(vendor_current_page-2>1){ + let firstDotElement = document.createElement('li') + firstDotElement.classList.add('uk-disabled') + firstDotElement.innerHTML = ``; + paginationElement.append(firstDotElement) + } + // last + if(vendor_current_page-2>0){ + let lastElement = document.createElement('li') + lastElement.innerHTML = `${vendor_current_page-1}` + paginationElement.append(lastElement) + } + // current + if(vendor_current_page!=1 && vendor_current_page != vendor_end_page){ + let currentElement = document.createElement('li') + currentElement.innerHTML = `
  • ${vendor_current_page}
  • ` + paginationElement.append(currentElement) + } + // next + if(vendor_current_page+2${vendor_current_page+1}` + paginationElement.append(nextElement) + } + // ... + if(vendor_current_page+2<=vendor_end_page){ + let secondDotElement = document.createElement('li') + secondDotElement.classList.add('uk-disabled') + secondDotElement.innerHTML = ``; + paginationElement.append(secondDotElement) + } + //end + let endElement = document.createElement('li') + if(vendor_current_page>=vendor_end_page){ + endElement.innerHTML = `${vendor_end_page}`; + endElement.classList.add('uk-disabled'); + }else { + endElement.innerHTML = `${vendor_end_page}`; + } + paginationElement.append(endElement) + //next button + let nextElement = document.createElement('li') + if(vendor_current_page>=vendor_end_page){ + nextElement.innerHTML = ``; + nextElement.classList.add('uk-disabled'); + }else { + nextElement.innerHTML = ``; + console.log(nextElement.innerHTML) + } + paginationElement.append(nextElement) +} + +// Select Vedor functions +let links_limit = 25 +let links_current_page = 1 +let links_end_page = 10 +async function getLinkedLists() { + const url = new URL('/receipt/getLinkedLists', window.location.origin); + url.searchParams.append('page', vendor_current_page); + url.searchParams.append('limit', vendor_limit); + const response = await fetch(url); + data = await response.json(); + links_end_page = data.end + return data.items; +} + +async function postLinkedItem(receipt_item_id, link_list_id, conv_factor) { + const response = await fetch(`/receipt/postLinkedItem`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + receipt_item_id: receipt_item_id, + link_list_id: link_list_id, + conv_factor: conv_factor + }), + }); + data = await response.json() + message_type = "primary" + if(data.error){ + message_type = "danger" + } + UIkit.notification({ + message: data.message, + status: message_type, + pos: 'top-right', + timeout: 5000 + }); + await refreshReceipt() + UIkit.modal(document.getElementById("linksModal")).hide(); +} + +async function openLinksSelectModal(receipt_item_id) { + let links = await getLinkedLists(); + await replenishLinksTableBody(links, receipt_item_id); + await updateLinksPaginationElement() + UIkit.modal(document.getElementById("linksModal")).show(); +} + +async function replenishLinksTableBody(links, receipt_item_id) { + let linksTableBody = document.getElementById('linksTableBody') + linksTableBody.innerHTML = "" + + for(let i=0; i < links.length; i++){ + let tableRow = document.createElement('tr') + + let idCell = document.createElement('td') + idCell.innerHTML = links[i].id + + let barcodeCell = document.createElement('td') + barcodeCell.innerHTML = links[i].barcode + + let nameCell = document.createElement('td') + nameCell.innerHTML = links[i].item_name + + let convFactorCell = document.createElement('td') + + let conv_factor_input = document.createElement('input') + conv_factor_input.setAttribute('class', 'uk-input') + conv_factor_input.setAttribute('id', `${links[i].id}_conv_factor`) + + convFactorCell.append(conv_factor_input) + + let addCell = document.createElement('td') + + let addbutton = document.createElement('button') + addbutton.setAttribute('class', 'uk-button') + addbutton.innerHTML = "Select" + addbutton.onclick = async function() { + let conv = document.getElementById(`${links[i].id}_conv_factor`) + if (!conv.value == ""){ + conv.classList.remove('uk-form-danger') + let conv_factor = parseFloat(conv.value) + await postLinkedItem(receipt_item_id, links[i].id, conv_factor) + } else { + conv.classList.add('uk-form-danger') + } + } + + addCell.append(addbutton) + + tableRow.append(idCell, barcodeCell, nameCell, convFactorCell, addCell) + linksTableBody.append(tableRow) + } + +} + +async function setLinksPage(pageNumber) { + links_current_page = pageNumber; + let links = await getLinkedLists() + await updateLinksPaginationElement() + await replenishLinksTableBody(links) +} + +async function updateLinksPaginationElement() { + let paginationElement = document.getElementById("linksPage"); + paginationElement.innerHTML = ""; + // previous + let previousElement = document.createElement('li') + if(links_current_page<=1){ + previousElement.innerHTML = ``; + previousElement.classList.add('uk-disabled'); + }else { + previousElement.innerHTML = ``; + } + paginationElement.append(previousElement) + + //first + let firstElement = document.createElement('li') + if(links_current_page<=1){ + firstElement.innerHTML = `1`; + firstElement.classList.add('uk-disabled'); + }else { + firstElement.innerHTML = `1`; + } + paginationElement.append(firstElement) + + // ... + if(links_current_page-2>1){ + let firstDotElement = document.createElement('li') + firstDotElement.classList.add('uk-disabled') + firstDotElement.innerHTML = ``; + paginationElement.append(firstDotElement) + } + // last + if(links_current_page-2>0){ + let lastElement = document.createElement('li') + lastElement.innerHTML = `${links_current_page-1}` + paginationElement.append(lastElement) + } + // current + if(links_current_page!=1 && links_current_page != links_end_page){ + let currentElement = document.createElement('li') + currentElement.innerHTML = `
  • ${links_current_page}
  • ` + paginationElement.append(currentElement) + } + // next + if(links_current_page+2${links_current_page+1}` + paginationElement.append(nextElement) + } + // ... + if(links_current_page+2<=links_end_page){ + let secondDotElement = document.createElement('li') + secondDotElement.classList.add('uk-disabled') + secondDotElement.innerHTML = ``; + paginationElement.append(secondDotElement) + } + //end + let endElement = document.createElement('li') + if(links_current_page>=links_end_page){ + endElement.innerHTML = `${links_end_page}`; + endElement.classList.add('uk-disabled'); + }else { + endElement.innerHTML = `${links_end_page}`; + } + paginationElement.append(endElement) + //next button + let nextElement = document.createElement('li') + if(links_current_page>=links_end_page){ + 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/static/handlers/receiptsHandler.js b/static/handlers/receiptsHandler.js index 95e3a7c..54d8a93 100644 --- a/static/handlers/receiptsHandler.js +++ b/static/handlers/receiptsHandler.js @@ -1,6 +1,32 @@ var pagination_current = 1; var pagination_end = 10 +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) +} + document.addEventListener('DOMContentLoaded', async function() { let receipts = await getReceipts() await replenishReceiptsTable(receipts) diff --git a/static/handlers/recipeViewHandler.js b/static/handlers/recipeViewHandler.js index 2a310dd..b577fee 100644 --- a/static/handlers/recipeViewHandler.js +++ b/static/handlers/recipeViewHandler.js @@ -74,7 +74,7 @@ async function replenishInstructions() { innerTile.setAttribute('class', 'uk-tile uk-tile-default uk-padding-small') let instruction = document.createElement('p') - instruction.innerHTML = `${recipe.instructions[i]}` + instruction.innerHTML = `Step ${i+1}: ${recipe.instructions[i]}` innerTile.append(instruction) diff --git a/static/handlers/transactionHandler.js b/static/handlers/transactionHandler.js index d42904b..0ecb98b 100644 --- a/static/handlers/transactionHandler.js +++ b/static/handlers/transactionHandler.js @@ -4,6 +4,32 @@ var defaqult_limit = 2; var pagination_end = 1; var item; +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 replenishItemsTable(items) { let itemsTableBody = document.getElementById("itemsTableBody") itemsTableBody.innerHTML = "" diff --git a/static/handlers/workshopHandler.js b/static/handlers/workshopHandler.js new file mode 100644 index 0000000..17a4612 --- /dev/null +++ b/static/handlers/workshopHandler.js @@ -0,0 +1,1092 @@ +async function changeSite(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) +} + +var mode = false +async function toggleDarkMode() { + let darkMode = document.getElementById("dark-mode"); + darkMode.disabled = !darkMode.disabled; + mode = !mode; + if(mode){ + document.getElementById('modeToggle').innerHTML = "light_mode" + document.getElementById('main_html').classList.add('uk-light') + + } else { + document.getElementById('modeToggle').innerHTML = "dark_mode" + document.getElementById('main_html').classList.remove('uk-light') + } +} + +if(session.user.flags.darkmode){ + toggleDarkMode() +} + + +document.addEventListener('DOMContentLoaded', async function() { + let zones = await fetchZones() + await updateZonesPagination() + await replensihZonesTable(zones) + + let locations = await fetchLocations() + await updateLocationsPagination() + await replenishLocationsTable(locations) + + let vendors = await fetchVendors() + await updateVendorsPagination() + await replenishVendorsTable(vendors) + + let brands = await fetchBrands() + await updateBrandsPagination() + await replenishBrandsTable(brands) + + let prefixes = await fetchPrefixes() + await updatePrefixesPagination() + await replenishPrefixesTable(prefixes) + +}); + +// ZONES TAB FUNCTIONS +let zones_current_page = 1 +let zones_end_page = 10 +let zones_limit = 25 +async function fetchZones(){ + const url = new URL('/workshop/getZones', window.location.origin) + url.searchParams.append('page', zones_current_page) + url.searchParams.append('limit', zones_limit) + const response = await fetch(url) + data = await response.json() + zones_end_page = data.end + return data.zones +} + +async function replensihZonesTable(zones){ + let zonesTableBody = document.getElementById('zonesTableBody') + zonesTableBody.innerHTML = "" + + for(let i=0; i < zones.length; i++){ + let tableRow = document.createElement('tr') + + + let idCell = document.createElement('td') + idCell.innerHTML = `${zones[i].id}` + let nameCell = document.createElement('td') + nameCell.innerHTML = `${zones[i].name}` + let descriptionCell = document.createElement('td') + descriptionCell.innerHTML = `${zones[i].description}` + let opCell = document.createElement('td') + opCell.innerHTML = `` + + let editOp = document.createElement('button') + editOp.setAttribute('class', 'uk-button uk-button-small uk-button-default') + editOp.innerHTML = "edit" + editOp.onclick = async function () { + await openEditZoneModal(zones[i]) + } + + opCell.append(editOp) + tableRow.append(idCell, nameCell, descriptionCell, opCell) + zonesTableBody.append(tableRow) + } +} + +async function updateZonesPagination() { + let paginationElement = document.getElementById("zonesPagination"); + paginationElement.innerHTML = ""; + // previous + let previousElement = document.createElement('li') + if(zones_current_page<=1){ + previousElement.innerHTML = ``; + previousElement.classList.add('uk-disabled'); + }else { + previousElement.innerHTML = ``; + } + paginationElement.append(previousElement) + + //first + let firstElement = document.createElement('li') + if(zones_current_page<=1){ + firstElement.innerHTML = `1`; + firstElement.classList.add('uk-disabled'); + }else { + firstElement.innerHTML = `1`; + } + paginationElement.append(firstElement) + + // ... + if(zones_current_page-2>1){ + let firstDotElement = document.createElement('li') + firstDotElement.classList.add('uk-disabled') + firstDotElement.innerHTML = ``; + paginationElement.append(firstDotElement) + } + // last + if(zones_current_page-2>0){ + let lastElement = document.createElement('li') + lastElement.innerHTML = `${zones_current_page-1}` + paginationElement.append(lastElement) + } + // current + if(zones_current_page!=1 && zones_current_page != zones_end_page){ + let currentElement = document.createElement('li') + currentElement.innerHTML = `
  • ${zones_current_page}
  • ` + paginationElement.append(currentElement) + } + // next + if(zones_current_page+2${zones_current_page+1}` + paginationElement.append(nextElement) + } + // ... + if(zones_current_page+2<=zones_end_page){ + let secondDotElement = document.createElement('li') + secondDotElement.classList.add('uk-disabled') + secondDotElement.innerHTML = ``; + paginationElement.append(secondDotElement) + } + //end + let endElement = document.createElement('li') + if(zones_current_page>=zones_end_page){ + endElement.innerHTML = `${zones_end_page}`; + endElement.classList.add('uk-disabled'); + }else { + endElement.innerHTML = `${zones_end_page}`; + } + paginationElement.append(endElement) + //next button + let nextElement = document.createElement('li') + if(zones_current_page>=zones_end_page){ + nextElement.innerHTML = ``; + nextElement.classList.add('uk-disabled'); + }else { + nextElement.innerHTML = ``; + } + paginationElement.append(nextElement) +} + +async function setZonePage(pageNumber){ + zones_current_page = pageNumber; + let zones = await fetchZones() + await updateZonesPagination() + await replensihZonesTable(zones) +} + +async function openAddZoneModal() { + document.getElementById('ZonesModalHeader').innerHTML = "Add Zone to system..." + document.getElementById('ZonesModalSubmitButton').innerHTML = "Add" + document.getElementById('ZoneName').value = "" + document.getElementById('ZoneName').classList.remove('uk-disabled') + document.getElementById('ZoneDescription').value = "" + document.getElementById('ZonesModalSubmitButton').onclick = async function() { + await postAddZone() + } + UIkit.modal(document.getElementById('ZonesModal')).show(); +} + +async function openEditZoneModal(zone) { + document.getElementById('ZonesModalHeader').innerHTML = `Edit Zone: ${zone.name}...` + document.getElementById('ZonesModalSubmitButton').innerHTML = "Save" + document.getElementById('ZoneName').value = zone.name + document.getElementById('ZoneName').classList.add('uk-disabled') + document.getElementById('ZoneDescription').value = zone.description + document.getElementById('ZonesModalSubmitButton').onclick = async function() { + await postEditZone(zone.id) + } + UIkit.modal(document.getElementById('ZonesModal')).show(); +} + +async function postAddZone() { + let zoneName = `${document.getElementById('ZoneName').value}` + let description = `${document.getElementById('ZoneDescription').value}` + + const response = await fetch(`/workshop/postAddZone`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: zoneName, + description: description, + }), + }); + 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 + }); + let zones = await fetchZones() + await updateZonesPagination() + await replensihZonesTable(zones) + UIkit.modal(document.getElementById('ZonesModal')).hide(); +} + +async function postEditZone(zone_id) { + let description = `${document.getElementById('ZoneDescription').value}` + + const response = await fetch(`/workshop/postEditZone`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + zone_id: zone_id, + update: {'description': description} + }), + }); + 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 + }); + let zones = await fetchZones() + await updateZonesPagination() + await replensihZonesTable(zones) + UIkit.modal(document.getElementById('ZonesModal')).hide(); +} + +// LOCATIONS TAB FUNCTIONS +let locations_current_page = 1 +let locations_end_page = 10 +let locations_limit = 25 +async function fetchLocations(){ + const url = new URL('/workshop/getLocations', window.location.origin) + url.searchParams.append('page', locations_current_page) + url.searchParams.append('limit', locations_limit) + const response = await fetch(url) + data = await response.json() + locations_end_page = data.end + return data.locations +} + +async function replenishLocationsTable(locations){ + let locationsTableBody = document.getElementById('locationsTableBody') + locationsTableBody.innerHTML = "" + + for(let i=0; i < locations.length; i++){ + let tableRow = document.createElement('tr') + + + let idCell = document.createElement('td') + idCell.innerHTML = `${locations[i].id}` + let uuidCell = document.createElement('td') + uuidCell.innerHTML = `${locations[i].uuid}` + let nameCell = document.createElement('td') + nameCell.innerHTML = `${locations[i].name}` + let descriptionCell = document.createElement('td') + descriptionCell.innerHTML = `` + let opCell = document.createElement('td') + opCell.innerHTML = `` + + tableRow.append(idCell, uuidCell,nameCell, descriptionCell, opCell) + locationsTableBody.append(tableRow) + } +} + +async function updateLocationsPagination() { + let paginationElement = document.getElementById("locationsPagination"); + paginationElement.innerHTML = ""; + // previous + let previousElement = document.createElement('li') + if(locations_current_page<=1){ + previousElement.innerHTML = ``; + previousElement.classList.add('uk-disabled'); + }else { + previousElement.innerHTML = ``; + } + paginationElement.append(previousElement) + + //first + let firstElement = document.createElement('li') + if(locations_current_page<=1){ + firstElement.innerHTML = `1`; + firstElement.classList.add('uk-disabled'); + }else { + firstElement.innerHTML = `1`; + } + paginationElement.append(firstElement) + + // ... + if(locations_current_page-2>1){ + let firstDotElement = document.createElement('li') + firstDotElement.classList.add('uk-disabled') + firstDotElement.innerHTML = ``; + paginationElement.append(firstDotElement) + } + // last + if(locations_current_page-2>0){ + let lastElement = document.createElement('li') + lastElement.innerHTML = `${locations_current_page-1}` + paginationElement.append(lastElement) + } + // current + if(locations_current_page!=1 && locations_current_page != locations_end_page){ + let currentElement = document.createElement('li') + currentElement.innerHTML = `
  • ${locations_current_page}
  • ` + paginationElement.append(currentElement) + } + // next + if(locations_current_page+2${locations_current_page+1}` + paginationElement.append(nextElement) + } + // ... + if(locations_current_page+2<=locations_end_page){ + let secondDotElement = document.createElement('li') + secondDotElement.classList.add('uk-disabled') + secondDotElement.innerHTML = ``; + paginationElement.append(secondDotElement) + } + //end + let endElement = document.createElement('li') + if(locations_current_page>=locations_end_page){ + endElement.innerHTML = `${locations_end_page}`; + endElement.classList.add('uk-disabled'); + }else { + endElement.innerHTML = `${locations_end_page}`; + } + paginationElement.append(endElement) + //next button + let nextElement = document.createElement('li') + if(locations_current_page>=locations_end_page){ + nextElement.innerHTML = ``; + nextElement.classList.add('uk-disabled'); + }else { + nextElement.innerHTML = ``; + } + paginationElement.append(nextElement) +} + +async function setLocationsPage(pageNumber){ + locations_current_page = pageNumber; + let locations = await fetchLocations() + await updateLocationsPagination() + await replenishLocationsTable(locations) +} + +async function openAddLocationModal() { + document.getElementById('LocationsModalHeader').innerHTML = "Add Location to system..." + document.getElementById('LocationsModalSubmitButton').innerHTML = "Add" + document.getElementById('LocationUUID').value = "" + document.getElementById('LocationName').value = "" + document.getElementById('LocationName').classList.remove('uk-disabled') + document.getElementById('LocationsModalSubmitButton').onclick = async function() { + await postAddLocation() + } + UIkit.modal(document.getElementById('LocationsModal')).show(); +} + +async function postAddLocation() { + let zone_id = parseInt(`${document.getElementById('LocationZoneId').value}`) + let locationName = `${document.getElementById('LocationName').value}` + let zone_name = document.getElementById(`locationzone_${zone_id}`).innerHTML + let uuid = `${zone_name}@${locationName}` + + const response = await fetch(`/workshop/postAddLocation`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + uuid: uuid, + name: locationName, + zone_id: zone_id + }), + }); + 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 + }); + let locations = await fetchLocations() + await updateLocationsPagination() + await replenishLocationsTable(locations) + UIkit.modal(document.getElementById('LocationsModal')).hide(); +} + +// VENDORS TAB FUNCTIONS +let vendors_current_page = 1 +let vendors_end_page = 10 +let vendors_limit = 25 +async function fetchVendors(){ + const url = new URL('/workshop/getVendors', window.location.origin) + url.searchParams.append('page', vendors_current_page) + url.searchParams.append('limit', vendors_limit) + const response = await fetch(url) + data = await response.json() + vendors_end_page = data.end + return data.vendors +} + +async function replenishVendorsTable(vendors){ + let vendorsTableBody = document.getElementById('vendorsTableBody') + vendorsTableBody.innerHTML = "" + + for(let i=0; i < vendors.length; i++){ + let tableRow = document.createElement('tr') + + let idCell = document.createElement('td') + idCell.innerHTML = `${vendors[i].id}` + let nameCell = document.createElement('td') + nameCell.innerHTML = `${vendors[i].vendor_name}` + let createdByCell = document.createElement('td') + createdByCell.innerHTML = `${vendors[i].created_by}` + let opCell = document.createElement('td') + opCell.innerHTML = `` + + let editOp = document.createElement('button') + editOp.innerHTML = "edit" + editOp.setAttribute('class', 'uk-button uk-button-small uk-button-default') + editOp.onclick = async function () { + await openEditVendorsModal(vendors[i]) + } + + opCell.append(editOp) + tableRow.append(idCell,nameCell, createdByCell, opCell) + vendorsTableBody.append(tableRow) + } +} + +async function updateVendorsPagination() { + let paginationElement = document.getElementById("vendorsPagination"); + paginationElement.innerHTML = ""; + // previous + let previousElement = document.createElement('li') + if(vendors_current_page<=1){ + previousElement.innerHTML = ``; + previousElement.classList.add('uk-disabled'); + }else { + previousElement.innerHTML = ``; + } + paginationElement.append(previousElement) + + //first + let firstElement = document.createElement('li') + if(vendors_current_page<=1){ + firstElement.innerHTML = `1`; + firstElement.classList.add('uk-disabled'); + }else { + firstElement.innerHTML = `1`; + } + paginationElement.append(firstElement) + + // ... + if(vendors_current_page-2>1){ + let firstDotElement = document.createElement('li') + firstDotElement.classList.add('uk-disabled') + firstDotElement.innerHTML = ``; + paginationElement.append(firstDotElement) + } + // last + if(vendors_current_page-2>0){ + let lastElement = document.createElement('li') + lastElement.innerHTML = `${vendors_current_page-1}` + paginationElement.append(lastElement) + } + // current + if(vendors_current_page!=1 && vendors_current_page != vendors_end_page){ + let currentElement = document.createElement('li') + currentElement.innerHTML = `
  • ${vendors_current_page}
  • ` + paginationElement.append(currentElement) + } + // next + if(vendors_current_page+2${vendors_current_page+1}` + paginationElement.append(nextElement) + } + // ... + if(vendors_current_page+2<=vendors_end_page){ + let secondDotElement = document.createElement('li') + secondDotElement.classList.add('uk-disabled') + secondDotElement.innerHTML = ``; + paginationElement.append(secondDotElement) + } + //end + let endElement = document.createElement('li') + if(vendors_current_page>=vendors_end_page){ + endElement.innerHTML = `${vendors_end_page}`; + endElement.classList.add('uk-disabled'); + }else { + endElement.innerHTML = `${vendors_end_page}`; + } + paginationElement.append(endElement) + //next button + let nextElement = document.createElement('li') + if(vendors_current_page>=vendors_end_page){ + nextElement.innerHTML = ``; + nextElement.classList.add('uk-disabled'); + }else { + nextElement.innerHTML = ``; + } + paginationElement.append(nextElement) +} + +async function setVendorsPage(pageNumber){ + vendors_current_page = pageNumber; + let vendors = await fetchVendors() + await updateVendorsPagination() + await replenishVendorsTable(vendors) +} + +async function openAddVendorsModal() { + document.getElementById('VendorsModalHeader').innerHTML = "Add Vendor to system..." + document.getElementById('VendorsModalSubmitButton').innerHTML = "Add" + document.getElementById('VendorName').value = "" + document.getElementById('VendorPhoneNumber').value = "" + document.getElementById('VendorAddress').value = "" + document.getElementById('vendor_created').value = "" + document.getElementById('vendor_created_by').value = "" + document.getElementById('VendorsModalSubmitButton').onclick = async function() { + await postAddVendor() + } + UIkit.modal(document.getElementById('VendorsModal')).show(); +} + +async function openEditVendorsModal(vendor) { + document.getElementById('VendorsModalHeader').innerHTML = `Edit Vendor: ${vendor.vendor_name}` + document.getElementById('VendorsModalSubmitButton').innerHTML = "Edit" + document.getElementById('VendorName').value = vendor.vendor_name + document.getElementById('VendorPhoneNumber').value = vendor.phone_number + document.getElementById('VendorAddress').value = vendor.vendor_address + document.getElementById('vendor_created').value = vendor.creation_date + document.getElementById('vendor_created_by').value = vendor.created_by + document.getElementById('VendorsModalSubmitButton').onclick = async function() { + await postEditVendor(vendor.id) + } + UIkit.modal(document.getElementById('VendorsModal')).show(); +} + +async function postAddVendor() { + let vendor_name = document.getElementById('VendorName').value + let vendor_phone_number = document.getElementById('VendorPhoneNumber').value + let vendor_address = document.getElementById('VendorAddress').value + + const response = await fetch(`/workshop/postAddVendor`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + vendor_name: vendor_name, + vendor_phone_number: vendor_phone_number, + vendor_address: vendor_address + }), + }); + 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 + }); + let vendors = await fetchVendors() + await updateVendorsPagination() + await replenishVendorsTable(vendors) + UIkit.modal(document.getElementById('VendorsModal')).hide(); +} + +async function postEditVendor(vendor_id) { + let vendor_name = document.getElementById('VendorName').value + let vendor_phone_number = document.getElementById('VendorPhoneNumber').value + let vendor_address = document.getElementById('VendorAddress').value + + const response = await fetch(`/workshop/postEditVendor`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + vendor_id: vendor_id, + update: {'vendor_name': vendor_name, 'phone_number': vendor_phone_number, 'vendor_address': vendor_address} + }), + }); + 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 + }); + let vendors = await fetchVendors() + await updateVendorsPagination() + await replenishVendorsTable(vendors) + UIkit.modal(document.getElementById('VendorsModal')).hide(); +} + +// BRANDS TAB FUNCTIONS +let brands_current_page = 1 +let brands_end_page = 10 +let brands_limit = 25 +async function fetchBrands(){ + const url = new URL('/workshop/getBrands', window.location.origin) + url.searchParams.append('page', brands_current_page) + url.searchParams.append('limit', brands_limit) + const response = await fetch(url) + data = await response.json() + brands_end_page = data.end + return data.brands +} + +async function replenishBrandsTable(brands){ + let brandsTableBody = document.getElementById('brandsTableBody') + brandsTableBody.innerHTML = "" + + for(let i=0; i < brands.length; i++){ + let tableRow = document.createElement('tr') + + let idCell = document.createElement('td') + idCell.innerHTML = `${brands[i].id}` + let nameCell = document.createElement('td') + nameCell.innerHTML = `${brands[i].name}` + let opCell = document.createElement('td') + opCell.innerHTML = `` + + let editOp = document.createElement('button') + editOp.innerHTML = "edit" + editOp.setAttribute('class', 'uk-button uk-button-small uk-button-default') + editOp.onclick = async function() { + await openEditBrandsModal(brands[i]) + } + + opCell.append(editOp) + tableRow.append(idCell,nameCell, opCell) + brandsTableBody.append(tableRow) + } +} + +async function updateBrandsPagination() { + let paginationElement = document.getElementById("brandsPagination"); + paginationElement.innerHTML = ""; + // previous + let previousElement = document.createElement('li') + if(brands_current_page<=1){ + previousElement.innerHTML = ``; + previousElement.classList.add('uk-disabled'); + }else { + previousElement.innerHTML = ``; + } + paginationElement.append(previousElement) + + //first + let firstElement = document.createElement('li') + if(brands_current_page<=1){ + firstElement.innerHTML = `1`; + firstElement.classList.add('uk-disabled'); + }else { + firstElement.innerHTML = `1`; + } + paginationElement.append(firstElement) + + // ... + if(brands_current_page-2>1){ + let firstDotElement = document.createElement('li') + firstDotElement.classList.add('uk-disabled') + firstDotElement.innerHTML = ``; + paginationElement.append(firstDotElement) + } + // last + if(brands_current_page-2>0){ + let lastElement = document.createElement('li') + lastElement.innerHTML = `${brands_current_page-1}` + paginationElement.append(lastElement) + } + // current + if(brands_current_page!=1 && brands_current_page != brands_end_page){ + let currentElement = document.createElement('li') + currentElement.innerHTML = `
  • ${brands_current_page}
  • ` + paginationElement.append(currentElement) + } + // next + if(brands_current_page+2${brands_current_page+1}` + paginationElement.append(nextElement) + } + // ... + if(brands_current_page+2<=brands_end_page){ + let secondDotElement = document.createElement('li') + secondDotElement.classList.add('uk-disabled') + secondDotElement.innerHTML = ``; + paginationElement.append(secondDotElement) + } + //end + let endElement = document.createElement('li') + if(brands_current_page>=brands_end_page){ + endElement.innerHTML = `${brands_end_page}`; + endElement.classList.add('uk-disabled'); + }else { + endElement.innerHTML = `${brands_end_page}`; + } + paginationElement.append(endElement) + //next button + let nextElement = document.createElement('li') + if(brands_current_page>=brands_end_page){ + nextElement.innerHTML = ``; + nextElement.classList.add('uk-disabled'); + }else { + nextElement.innerHTML = ``; + } + paginationElement.append(nextElement) +} + +async function setBrandsPage(pageNumber){ + brands_current_page = pageNumber; + let brands = await fetchBrands() + await updateBrandsPagination() + await replenishBrandsTable(brands) +} + +async function openAddBrandsModal() { + document.getElementById('BrandsModalHeader').innerHTML = "Add Vendor to system..." + document.getElementById('BrandsModalSubmitButton').innerHTML = "Add" + document.getElementById('BrandName').value = "" + document.getElementById('BrandsModalSubmitButton').onclick = async function() { + await postAddBrand() + } + UIkit.modal(document.getElementById('BrandsModal')).show(); +} + +async function openEditBrandsModal(brand) { + document.getElementById('BrandsModalHeader').innerHTML = `Edit Brand: ${brand.name}` + document.getElementById('BrandsModalSubmitButton').innerHTML = "Edit" + document.getElementById('BrandName').value = brand.name + + document.getElementById('BrandsModalSubmitButton').onclick = async function() { + await postEditBrand(brand.id) + } + UIkit.modal(document.getElementById('BrandsModal')).show(); +} + +async function postAddBrand() { + let brand_name = document.getElementById('BrandName').value + + const response = await fetch(`/workshop/postAddBrand`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + brand_name: brand_name + }), + }); + 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 + }); + let brands = await fetchBrands() + await updateBrandsPagination() + await replenishBrandsTable(brands) + UIkit.modal(document.getElementById('BrandsModal')).hide(); +} + +async function postEditBrand(brand_id) { + let brand_name = document.getElementById('BrandName').value + + const response = await fetch(`/workshop/postEditBrand`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + brand_id: brand_id, + update: {'name': brand_name} + }), + }); + 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 + }); + let brands = await fetchBrands() + await updateBrandsPagination() + await replenishBrandsTable(brands) + UIkit.modal(document.getElementById('BrandsModal')).hide(); +} + +// PREFIXES TAB FUNCTIONS +let prefix_current_page = 1 +let prefix_end_page = 10 +let prefix_limit = 25 +async function fetchPrefixes(){ + const url = new URL('/workshop/getPrefixes', window.location.origin) + url.searchParams.append('page', prefix_current_page) + url.searchParams.append('limit', prefix_limit) + const response = await fetch(url) + data = await response.json() + prefix_end_page = data.end + return data.prefixes +} + +async function replenishPrefixesTable(prefixes){ + let prefixesTableBody = document.getElementById('prefixesTableBody') + prefixesTableBody.innerHTML = "" + + for(let i=0; i < prefixes.length; i++){ + let tableRow = document.createElement('tr') + + let idCell = document.createElement('td') + idCell.innerHTML = `${prefixes[i].id}` + let uuidCell = document.createElement('td') + uuidCell.innerHTML = `${prefixes[i].uuid}` + let nameCell = document.createElement('td') + nameCell.innerHTML = `${prefixes[i].name}` + let descriptionCell = document.createElement('td') + descriptionCell.innerHTML = `${prefixes[i].description}` + let opCell = document.createElement('td') + opCell.innerHTML = `` + + let editOp = document.createElement('button') + editOp.innerHTML = "edit" + editOp.setAttribute('class', 'uk-button uk-button-small uk-button-default') + editOp.onclick = async function() { + await openEditPrefixModal(prefixes[i]) + } + + opCell.append(editOp) + + tableRow.append(idCell,uuidCell, nameCell, descriptionCell, opCell) + prefixesTableBody.append(tableRow) + } +} + +async function updatePrefixesPagination() { + let paginationElement = document.getElementById("prefixesPagination"); + paginationElement.innerHTML = ""; + // previous + let previousElement = document.createElement('li') + if(prefix_current_page<=1){ + previousElement.innerHTML = ``; + previousElement.classList.add('uk-disabled'); + }else { + previousElement.innerHTML = ``; + } + paginationElement.append(previousElement) + + //first + let firstElement = document.createElement('li') + if(prefix_current_page<=1){ + firstElement.innerHTML = `1`; + firstElement.classList.add('uk-disabled'); + }else { + firstElement.innerHTML = `1`; + } + paginationElement.append(firstElement) + + // ... + if(prefix_current_page-2>1){ + let firstDotElement = document.createElement('li') + firstDotElement.classList.add('uk-disabled') + firstDotElement.innerHTML = ``; + paginationElement.append(firstDotElement) + } + // last + if(prefix_current_page-2>0){ + let lastElement = document.createElement('li') + lastElement.innerHTML = `${prefix_current_page-1}` + paginationElement.append(lastElement) + } + // current + if(prefix_current_page!=1 && prefix_current_page != prefix_end_page){ + let currentElement = document.createElement('li') + currentElement.innerHTML = `
  • ${prefix_current_page}
  • ` + paginationElement.append(currentElement) + } + // next + if(prefix_current_page+2${prefix_current_page+1}` + paginationElement.append(nextElement) + } + // ... + if(prefix_current_page+2<=prefix_end_page){ + let secondDotElement = document.createElement('li') + secondDotElement.classList.add('uk-disabled') + secondDotElement.innerHTML = ``; + paginationElement.append(secondDotElement) + } + //end + let endElement = document.createElement('li') + if(prefix_current_page>=prefix_end_page){ + endElement.innerHTML = `${prefix_end_page}`; + endElement.classList.add('uk-disabled'); + }else { + endElement.innerHTML = `${prefix_end_page}`; + } + paginationElement.append(endElement) + //next button + let nextElement = document.createElement('li') + if(prefix_current_page>=prefix_end_page){ + nextElement.innerHTML = ``; + nextElement.classList.add('uk-disabled'); + }else { + nextElement.innerHTML = ``; + } + paginationElement.append(nextElement) +} + +async function setPrefixesPage(pageNumber){ + prefix_current_page = pageNumber; + let prefixes = await fetchPrefixes() + await updatePrefixesPagination() + await replenishPrefixesTable(prefixes) +} + +async function openAddPrefixModal() { + document.getElementById('PrefixModalHeader').innerHTML = "Add Prefix to system..." + document.getElementById('PrefixModalSubmitButton').innerHTML = "Add" + document.getElementById('PrefixUUID').value = "" + document.getElementById('PrefixName').value = "" + document.getElementById('PrefixDescription').value = "" + document.getElementById('PrefixModalSubmitButton').onclick = async function() { + await postAddPrefix() + } + UIkit.modal(document.getElementById('PrefixModal')).show(); +} + +async function openEditPrefixModal(prefix) { + document.getElementById('PrefixModalHeader').innerHTML = `Edit Prefix: ${prefix.name}` + document.getElementById('PrefixModalSubmitButton').innerHTML = "Edit" + document.getElementById('PrefixUUID').value = prefix.uuid + document.getElementById('PrefixName').value = prefix.name + document.getElementById('PrefixDescription').value = prefix.description + + document.getElementById('PrefixModalSubmitButton').onclick = async function() { + await postEditPrefix(prefix.id) + } + UIkit.modal(document.getElementById('PrefixModal')).show(); +} + +async function postAddPrefix() { + let prefix_uuid = document.getElementById('PrefixUUID').value + let prefix_name = document.getElementById('PrefixName').value + let prefix_description = document.getElementById('PrefixDescription').value + + const response = await fetch(`/workshop/postAddPrefix`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + prefix_uuid: prefix_uuid, + prefix_name:prefix_name, + prefix_description:prefix_description + }), + }); + 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 + }); + let prefixes = await fetchPrefixes() + await updatePrefixesPagination() + await replenishPrefixesTable(prefixes) + UIkit.modal(document.getElementById('PrefixModal')).hide(); +} + +async function postEditPrefix(prefix_id) { + let prefix_uuid = document.getElementById('PrefixUUID').value + let prefix_name = document.getElementById('PrefixName').value + let prefix_description = document.getElementById('PrefixDescription').value + + const response = await fetch(`/workshop/postEditPrefix`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + prefix_id: prefix_id, + update: {'name': prefix_name, 'uuid': prefix_uuid, 'description': prefix_description} + }), + }); + 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 + }); + let prefixes = await fetchPrefixes() + await updatePrefixesPagination() + await replenishPrefixesTable(prefixes) + UIkit.modal(document.getElementById('PrefixModal')).hide(); +} \ No newline at end of file diff --git a/static/js/register_service_worker.js b/static/js/register_service_worker.js new file mode 100644 index 0000000..ac35c9d --- /dev/null +++ b/static/js/register_service_worker.js @@ -0,0 +1,84 @@ +'use strict'; + +function urlB64ToUint8Array(base64String) { + const padding = '='.repeat((4 - base64String.length % 4) % 4); + const base64 = (base64String + padding) + .replace(/\-/g, '+') + .replace(/_/g, '/'); + + const rawData = window.atob(base64); + const outputArray = new Uint8Array(rawData.length); + + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + return outputArray; +} + +function updateSubscriptionOnServer(subscription, apiEndpoint) { + // TODO: Send subscription to application server + + return fetch(apiEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + subscription_json: JSON.stringify(subscription) + }) + }); + +} + +async function subscribeUser(swRegistration, applicationServerPublicKey, apiEndpoint) { + if (swRegistration.active) { + console.log("executing Sub.") + return executeSubscription(swRegistration, applicationServerPublicKey, apiEndpoint); + } else { + swRegistration.addEventListener('statechange', function(event) { + if (event.target.state === 'activated') { + console.log('waiting and sub.') + return executeSubscription(swRegistration, applicationServerPublicKey, apiEndpoint); + } + }); + } +} + +async function executeSubscription(swRegistration, applicationServerPublicKey, apiEndpoint) { + const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey); + try { + const subscription = await swRegistration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: applicationServerKey + }); + console.log('User is subscribed.'); + const response = await updateSubscriptionOnServer(subscription, apiEndpoint); + if (!response.ok) throw new Error('Bad status code from server.'); + const responseData = await response.json(); + if (responseData.status !== "success") throw new Error('Bad response from server.'); + console.log(responseData); + } catch (err) { + console.log('Failed to subscribe the user: ', err); + console.log(err.stack); + } +} + + +async function registerServiceWorker(serviceWorkerUrl, applicationServerPublicKey, apiEndpoint){ + let swRegistration = null; + if ('serviceWorker' in navigator && 'PushManager' in window) { + console.log('Service Worker and Push is supported'); + + try { + const swReg = await navigator.serviceWorker.register(serviceWorkerUrl); + console.log('Service Worker is registered', swReg); + await subscribeUser(swReg, applicationServerPublicKey, apiEndpoint); + swRegistration = swReg; + } catch (error) { + console.error('Service Worker Error', error); + } + } else { + console.warn('Push messaging is not supported'); + } + return swRegistration; +} \ No newline at end of file diff --git a/static/js/service_worker.js b/static/js/service_worker.js new file mode 100644 index 0000000..5b989e1 --- /dev/null +++ b/static/js/service_worker.js @@ -0,0 +1,35 @@ + +'use strict'; + +/* eslint-enable max-len */ + +self.addEventListener('install', function(event) { + console.log('Service Worker installing.'); +}); + +self.addEventListener('activate', function(event) { + console.log('Service Worker activating.'); +}); + +self.addEventListener('push', function(event) { + console.log('[Service Worker] Push Received.'); + const pushData = event.data.text(); + console.log(`[Service Worker] Push received this data - "${pushData}"`); + let data, title, body; + try { + data = JSON.parse(pushData); + title = data.title; + body = data.body; + } catch(e) { + title = "Untitled"; + body = pushData; + } + const options = { + body: body + }; + console.log(title, options); + + event.waitUntil( + self.registration.showNotification(title, options) + ); +}); \ No newline at end of file diff --git a/static/pictures/recipes/20250419_141111.jpg b/static/pictures/recipes/20250419_141111.jpg new file mode 100644 index 0000000..0e44e76 Binary files /dev/null and b/static/pictures/recipes/20250419_141111.jpg differ diff --git a/static/pictures/recipes/a2fca658c8fb498ab17ca9ba93385bba_h5kgg4.avif b/static/pictures/recipes/a2fca658c8fb498ab17ca9ba93385bba_h5kgg4.avif new file mode 100644 index 0000000..508c864 Binary files /dev/null and b/static/pictures/recipes/a2fca658c8fb498ab17ca9ba93385bba_h5kgg4.avif differ diff --git a/task_manager.py b/task_manager.py new file mode 100644 index 0000000..698fad9 --- /dev/null +++ b/task_manager.py @@ -0,0 +1,22 @@ +import schedule, time, psycopg2 +import postsqldb +from config import config + +def createCycleCount(): + print("task is running") + database_config = config() + with psycopg2.connect(**database_config) as conn: + sites = postsqldb.SitesTable.selectTuples(conn) + print(sites) + + conn.rollback() + +def start_schedule(): + schedule.every(1).minutes.do(createCycleCount) + + while True: + schedule.run_pending() + time.sleep(60) + + +createCycleCount() \ No newline at end of file diff --git a/templates/groups/group.html b/templates/groups/group.html deleted file mode 100644 index 1f1f5d4..0000000 --- a/templates/groups/group.html +++ /dev/null @@ -1,352 +0,0 @@ - - - - - My Pantry - Groups - - - - - - - - - - - - - - - -
    -
    -
    -
    -
    -
    -

    menu

    -
    -
    -

    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - -
    -
    - - -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    - - - - \ No newline at end of file diff --git a/templates/groups/index.html b/templates/groups/index.html deleted file mode 100644 index 5ca8215..0000000 --- a/templates/groups/index.html +++ /dev/null @@ -1,140 +0,0 @@ - - - - - Groups - - - - - - - - - - - - - - - - - - - -
    - -
    -
    -
    -
    -
    - - -
    -
    - -
    -
    -
    - -
    - -
    -
    -
    -
    - -
    -
    -

    -

    -

    - - -

    -
    -
    - - - - - - \ No newline at end of file diff --git a/templates/items/index.html b/templates/items/index.html index 237066d..1141082 100644 --- a/templates/items/index.html +++ b/templates/items/index.html @@ -32,17 +32,30 @@
  • Apps
  • Shopping Lists
  • -
  • Groups
  • Recipes
  • Logistics
  • Items
    You are currently browsing items here...
    -
    +
  • Add Transaction
  • +
  • Workshop
  • Receipts
  • System Management
  • +
  • {{current_site}}
    This is the current site you are viewing...
    +
    +
      + {% for site in sites %} + {% if site == current_site %} +
    • {{site}}
    • + {% else %} +
    • {{site}}
    • + {% endif %} + {% endfor %} +
    +
    +
  • {% if system_admin %}
  • Administration
  • {% endif %} @@ -52,7 +65,7 @@
    - + Menu
      @@ -111,7 +124,23 @@
  • Add Transaction
  • +
  • Workshop
  • Receipts
  • System Management
  • {% if system_admin %} @@ -363,6 +363,8 @@ + + @@ -456,6 +458,7 @@ +

    Select Location

    @@ -627,6 +630,7 @@
    +

    Select Item

    @@ -655,6 +659,37 @@
    + +
    +
    +

    Select Item

    +

    Select an Prefix from the system...

    + + + + + + + + + + +
    ZoneName
    +
    +
    + + diff --git a/templates/items/itemlink.html b/templates/items/itemlink.html index 1c273c8..a55b93f 100644 --- a/templates/items/itemlink.html +++ b/templates/items/itemlink.html @@ -39,7 +39,8 @@
    Items
    You are currently editing a linked item...
    -
    + +
  • Workshop
  • Add Transaction
  • Receipts
  • System Management
  • diff --git a/templates/items/transactions.html b/templates/items/transactions.html index ac239ae..928ef6b 100644 --- a/templates/items/transactions.html +++ b/templates/items/transactions.html @@ -39,8 +39,9 @@
    Items
    You are currently viewing transactions...
    - +
  • Add Transaction
  • +
  • Workshop
  • Receipts
  • System Management
  • {% if system_admin %} diff --git a/templates/nav.html b/templates/nav.html deleted file mode 100644 index e69de29..0000000 diff --git a/templates/other/login.html b/templates/other/login.html index 674c54a..bd77adb 100644 --- a/templates/other/login.html +++ b/templates/other/login.html @@ -47,7 +47,7 @@
    - +
    diff --git a/templates/other/transaction.html b/templates/other/transaction.html index be52f85..b5b35f3 100644 --- a/templates/other/transaction.html +++ b/templates/other/transaction.html @@ -36,7 +36,6 @@
  • Apps
  • Shopping Lists
  • -
  • Groups
  • Recipes
  • Logistics
  • Items
  • @@ -44,9 +43,23 @@
    Add Transaction
    You are adding transactions...
    - + +
  • Workshop
  • Receipts
  • System Management
  • +
  • {{current_site}}
    This is the current site you are viewing...
    +
    +
      + {% for site in sites %} + {% if site == current_site %} +
    • {{site}}
    • + {% else %} +
    • {{site}}
    • + {% endif %} + {% endfor %} +
    +
    +
  • {% if system_admin %}
  • Administration
  • {% endif %} @@ -56,11 +69,23 @@
    - + Menu
      -
    • {{current_site}}
    • +
    • {{current_site}} +
      +
        + {% for site in sites %} + {% if site == current_site %} +
      • {{site}}
      • + {% else %} +
      • {{site}}
      • + {% endif %} + {% endfor %} +
      +
      +
    • Logistics
    • Items
    • Add Transaction
    • diff --git a/templates/other/workshop.html b/templates/other/workshop.html new file mode 100644 index 0000000..957a8f4 --- /dev/null +++ b/templates/other/workshop.html @@ -0,0 +1,485 @@ + + + + + + + + + + + + + + + + + + +
      + + +
      +
      +
      +
      +
      + +
      +
      + +
      +
      +
      + +
      +
      + Here are all the Zones that have been set up for the site {{current_site}} + + + + + + + + + + + +
      IDZone NameDescriptionOperations
      + +
      +
      +
      + +
      +
      +
      + +
      +
      + Here are all the locations that have been set up for the site {{current_site}} + + + + + + + + + + + + +
      IDUUIDLocation NameDescriptionOperations
      + +
      +
      +
      + +
      +
      +
      + +
      +
      + Here are all the locations that have been set up for the site {{current_site}} + + + + + + + + + + + +
      IDVendor NameCreated ByOperations
      + +
      +
      +
      + +
      +
      +
      + +
      +
      + Here are all the locations that have been set up for the site {{current_site}} + + + + + + + + + + +
      IDBrand NameOperations
      + +
      +
      +
      + +
      +
      +
      + +
      +
      + Here are all the locations that have been set up for the site {{current_site}} + + + + + + + + + + + + +
      IDUUIDNameDescriptionOperations
      + +
      +
      +
      +
      +
      +
      +
      +
      +
      + + +
      +
      + +
      +

      +
      +
      +
      +
      +
      +
      +
      + + + +
      +
      +
      +
      + + +
      +
      +
      +
      + +
      +
      + +
      +
      + +
      +

      +
      +
      +
      +
      +
      +
      +
      + + +
      +
      +
      +
      + + +
      +
      +
      +
      + + +
      +
      +
      +
      + +
      +
      + +
      +
      + +
      +

      +
      +
      +
      +
      +
      +
      +
      + + +
      +
      +
      +
      + + +
      +
      +
      +
      + + +
      +
      +
      +
      + + +
      +
      +
      +
      + + +
      +
      +
      +
      + +
      +
      + +
      +
      + +
      +

      +
      +
      +
      +
      +
      +
      +
      + + +
      +
      +
      +
      + +
      +
      + +
      +
      + +
      +

      +
      +
      +
      +
      +
      +
      +
      + + +
      +
      +
      +
      + + +
      +
      +
      +
      + + +
      +
      +
      +
      + +
      +
      + + + {% assets "js_all" %} + + {% endassets %} + + + \ No newline at end of file diff --git a/templates/receipts/index.html b/templates/receipts/index.html index 2dbd8ec..744c85e 100644 --- a/templates/receipts/index.html +++ b/templates/receipts/index.html @@ -31,13 +31,26 @@
    • Apps
    • Shopping Lists
    • -
    • Groups
    • Recipes
    • Logistics
    • Items
    • Add Transaction
    • +
    • Workshop
    • Receipts
      You are currently viewing Receipts...
    • System Management
    • +
    • {{current_site}}
      This is the current site you are viewing...
      +
      +
        + {% for site in sites %} + {% if site == current_site %} +
      • {{site}}
      • + {% else %} +
      • {{site}}
      • + {% endif %} + {% endfor %} +
      +
      +
    • {% if system_admin %}
    • Administration
    • {% endif %} @@ -47,11 +60,23 @@
    - + Menu
      -
    • {{current_site}}
    • +
    • {{current_site}} +
      +
        + {% for site in sites %} + {% if site == current_site %} +
      • {{site}}
      • + {% else %} +
      • {{site}}
      • + {% endif %} + {% endfor %} +
      +
      +
    • Logistics
    • Receipts
    diff --git a/templates/receipts/receipt.html b/templates/receipts/receipt.html index 361e931..2b1ea1e 100644 --- a/templates/receipts/receipt.html +++ b/templates/receipts/receipt.html @@ -33,11 +33,11 @@
  • Apps
  • Shopping Lists
  • -
  • Groups
  • Recipes
  • Logistics
  • Items
  • Add Transaction
  • +
  • Workshop
  • Receipts
    You are currently editing a Receipt...
  • System Management
  • {% if system_admin %} @@ -76,7 +76,10 @@
    -

    This is the basic information that is attached to your receipt.

    +

    This is the basic information that is attached to your receipt. You can change the Vendor associated, manipulate your receipt lines, + and attach files to a receipt. Once a receipt has been fully resolved there is no way to reopen it without admin help so be sure to only + resolve the receipt once you are sure you no longer need to change its information. +

    @@ -105,35 +108,31 @@
    - +
    -
    -
    -
    - -
    - -
    -
    -
    - -
    +
    +
    + + +
    +
    +
    - +
    - +
    - +
    @@ -149,10 +148,12 @@
    -

    Here is where all the lines on this receipt are added, changed, manipulated, and resolved.

    +

    Here is where all the lines on this receipt are added, changed, manipulated, and resolved. Once a Line + has been resolved, deleted, denied it cannot be opened again so proceed with caution. +

    - +
    • Custom
    • @@ -220,38 +221,14 @@

      You may upload whatever files you may want to attach to a receipt to later download and view them.

    -
    +
    - +
    -
    -
    -
    - File Preview -
    -
    -

    File Name

    -

    Size: 1.5 MB

    -

    Description: A brief description of the file's contents.

    -
    -
    -
    -
    -
    -
    - File Preview -
    -
    -

    File Name

    -

    Size: 1.5 MB

    -

    Description: A brief description of the file's contents.

    -
    -
    -
    @@ -262,7 +239,7 @@
    -
    +

    Select Item

    Select an Item from the system...

    @@ -291,8 +268,40 @@
    + +
    +
    +

    Select Linked List

    +

    Select an Linked List from the system...

    + + + + + + + + + + + + + +
    IDBarcodeNameConversion FactorOperations
    +
    +
    -
    +

    Edit Line...

    Edit any fields here for the selected Line and then save them.

    @@ -349,6 +358,37 @@
    + +
    +
    +

    Select Vendor

    +

    Select an Vendor from the system...

    + + + + + + + + + + + + +
    IDNamePhone #Address
    +
    +
    diff --git a/templates/recipes/index.html b/templates/recipes/index.html index 699d380..0d4fe8b 100644 --- a/templates/recipes/index.html +++ b/templates/recipes/index.html @@ -39,8 +39,22 @@
  • Logistics
  • Items
  • Add Transaction
  • +
  • Workshop
  • Receipts
  • System Management
  • +
  • {{current_site}}
    This is the current site you are viewing...
    +
    +
      + {% for site in sites %} + {% if site == current_site %} +
    • {{site}}
    • + {% else %} +
    • {{site}}
    • + {% endif %} + {% endfor %} +
    +
    +
  • {% if system_admin %}
  • Administration
  • {% endif %} diff --git a/templates/recipes/recipe_edit.html b/templates/recipes/recipe_edit.html index 4bc64ef..d79ac9e 100644 --- a/templates/recipes/recipe_edit.html +++ b/templates/recipes/recipe_edit.html @@ -40,6 +40,7 @@
  • Logistics
  • Items
  • Add Transaction
  • +
  • Workshop
  • Receipts
  • System Management
  • {% if system_admin %} diff --git a/templates/recipes/recipe_view.html b/templates/recipes/recipe_view.html index 76d4b94..1d99e71 100644 --- a/templates/recipes/recipe_view.html +++ b/templates/recipes/recipe_view.html @@ -47,6 +47,7 @@
  • Logistics
  • Items
  • Add Transaction
  • +
  • Workshop
  • Receipts
  • System Management
  • {% if system_admin %} diff --git a/templates/shopping-lists/edit.html b/templates/shopping-lists/edit.html index 8c00030..16333a2 100644 --- a/templates/shopping-lists/edit.html +++ b/templates/shopping-lists/edit.html @@ -42,6 +42,7 @@
  • Logistics
  • Items
  • Add Transaction
  • +
  • Workshop
  • Receipts
  • System Management
  • {% if system_admin %} diff --git a/templates/shopping-lists/index.html b/templates/shopping-lists/index.html index 6c9e13a..8b34b82 100644 --- a/templates/shopping-lists/index.html +++ b/templates/shopping-lists/index.html @@ -37,13 +37,26 @@
    Shopping Lists
    You are currently browsing shopping lists here...
    -
  • Groups
  • Recipes
  • Logistics
  • Items
  • Add Transaction
  • +
  • Workshop
  • Receipts
  • System Management
  • +
  • {{current_site}}
    This is the current site you are viewing...
    +
    +
      + {% for site in sites %} + {% if site == current_site %} +
    • {{site}}
    • + {% else %} +
    • {{site}}
    • + {% endif %} + {% endfor %} +
    +
    +
  • {% if system_admin %}
  • Administration
  • {% endif %} @@ -53,7 +66,7 @@
    - + Menu
      diff --git a/templates/shopping-lists/view.html b/templates/shopping-lists/view.html index 6a98c1c..9023292 100644 --- a/templates/shopping-lists/view.html +++ b/templates/shopping-lists/view.html @@ -42,6 +42,7 @@
    • Logistics
    • Items
    • Add Transaction
    • +
    • Workshop
    • Receipts
    • System Management
    • {% if system_admin %} diff --git a/templates/subscribe.html b/templates/subscribe.html new file mode 100644 index 0000000..1a791d4 --- /dev/null +++ b/templates/subscribe.html @@ -0,0 +1,20 @@ + + + + + + +

      Webpush Demo

      + + + + \ No newline at end of file diff --git a/test.py b/test.py index 7a904fe..5cb70b4 100644 --- a/test.py +++ b/test.py @@ -4,18 +4,4 @@ import random, uuid, csv, postsqldb import pdf2image, os, pymupdf, PIL -def create_pdf_preview(pdf_path, output_path, size=(128, 128)): - pdf = pymupdf.open(pdf_path) - page = pdf[0] - file_name = os.path.basename(pdf_path).replace('.pdf', "") - pix = page.get_pixmap() - img = PIL.Image.frombytes("RGB", (pix.width, pix.height), pix.samples) - output_path = output_path + file_name + '.jpg' - img.thumbnail(size) - img.save(output_path) - - -file_path = 'static/files/receipts/Order_details_-_Walmart.com_04122025.pdf' -output_path = "static/files/receipts/previews/" - -create_pdf_preview(file_path, output_path) \ No newline at end of file +from pywebpush import webpush, WebPushException \ No newline at end of file diff --git a/webpush.py b/webpush.py new file mode 100644 index 0000000..2f6fc4a --- /dev/null +++ b/webpush.py @@ -0,0 +1,53 @@ +from pywebpush import webpush, WebPushException +import json, psycopg2,requests +from flask import current_app +import postsqldb, config + + +def push_ntfy(title, body): + requests.post("http://ntfy.treehousefullofstars.com/pantry", + data=body, + headers={ + "Title": title, + "Priority": "default", + "Tags": "tada" + }) + +def push_notifications(title, body): + database_config = config.config() + subscriptions = None + with psycopg2.connect(**database_config) as conn: + with conn.cursor() as cur: + cur.execute(f"SELECT * FROM push_subscriptions;") + subscriptions = cur.fetchall() + trigger_push_notifications_for_subscriptions(subscriptions, title, body) + +def trigger_push_notification(subscription, title, body): + print('sub', json.loads(subscription[1])) + try: + response = webpush( + subscription_info=json.loads(subscription[1]), + data=json.dumps({"title": title, "body": body}), + vapid_private_key=current_app.config["VAPID_PRIVATE_KEY"], + vapid_claims={ + "sub": "mailto:{}".format( + current_app.config["VAPID_CLAIM_EMAIL"]) + } + ) + print('response', response) + return response.ok + except WebPushException as ex: + if ex.response and ex.response.json(): + extra = ex.response.json() + print("Remote service replied with a {}:{}, {}", + extra.code, + extra.errno, + extra.message + ) + print(ex) + return False + + +def trigger_push_notifications_for_subscriptions(subscriptions, title, body): + return [trigger_push_notification(subscription, title, body) + for subscription in subscriptions] \ No newline at end of file diff --git a/webserver.py b/webserver.py index e35f744..e6dc9f1 100644 --- a/webserver.py +++ b/webserver.py @@ -1,16 +1,22 @@ -from flask import Flask, render_template, session, request, redirect +import celery.schedules +from flask import Flask, render_template, session, request, redirect, jsonify from flask_assets import Environment, Bundle import api, config, user_api, psycopg2, main, admin, item_API, receipts_API, shopping_list_API, group_api, recipes_api from user_api import login_required from external_API import external_api +from workshop_api import workshop_api import database import postsqldb +from webpush import trigger_push_notifications_for_subscriptions -app = Flask(__name__) +app = Flask(__name__, instance_relative_config=True) UPLOAD_FOLDER = 'static/pictures' FILES_FOLDER = 'static/files' +app.config.from_pyfile('application.cfg.py') app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER app.config['FILES_FOLDER'] = FILES_FOLDER + + assets = Environment(app) app.secret_key = '11gs22h2h1a4h6ah8e413a45' app.register_blueprint(api.database_api) @@ -18,6 +24,7 @@ app.register_blueprint(user_api.login_app) app.register_blueprint(admin.admin) app.register_blueprint(item_API.items_api) app.register_blueprint(external_api) +app.register_blueprint(workshop_api) app.register_blueprint(receipts_API.receipt_api) app.register_blueprint(shopping_list_API.shopping_list_api) app.register_blueprint(group_api.groups_api) @@ -80,15 +87,6 @@ def transaction(): units = postsqldb.UnitsTable.getAll(conn) return render_template("other/transaction.html", units=units, current_site=session['selected_site'], sites=sites, proto={'referrer': request.referrer}) -@app.route("/admin") -@login_required -def workshop(): - sites = [site[1] for site in main.get_sites(session['user']['sites'])] - print(session.get('user')['system_admin']) - if not session.get('user')['system_admin']: - return redirect('/logout') - return render_template("admin.html", current_site=session['selected_site'], sites=sites) - @app.route("/items") @login_required def items(): @@ -97,6 +95,24 @@ def items(): current_site=session['selected_site'], sites=sites) +@app.route("/api/push-subscriptions", methods=["POST"]) +def create_push_subscription(): + json_data = request.get_json() + database_config = config.config() + with psycopg2.connect(**database_config) as conn: + with conn.cursor() as cur: + cur.execute(f"SELECT * FROM push_subscriptions WHERE subscription_json = %s;", (json_data['subscription_json'],)) + rows = cur.fetchone() + if rows is None: + cur.execute(f"INSERT INTO push_subscriptions (subscription_json) VALUES (%s);", (json_data['subscription_json'],)) + return jsonify({ + "status": "success" + }) + +@app.route("/subscribe") +def subscribe(): + return render_template("subscribe.html") + @app.route("/") @login_required def home(): diff --git a/workshop_api.py b/workshop_api.py new file mode 100644 index 0000000..da6e357 --- /dev/null +++ b/workshop_api.py @@ -0,0 +1,269 @@ +from flask import Blueprint, request, render_template, redirect, session, url_for, send_file, jsonify, Response +import psycopg2, math, json, datetime, main, copy, requests, process, database, pprint, MyDataclasses +from config import config, sites_config +from main import unfoldCostLayers +from user_api import login_required +import postsqldb + +workshop_api = Blueprint('workshop_api', __name__) + +@workshop_api.route("/workshop") +@login_required +def workshop(): + sites = [site[1] for site in main.get_sites(session['user']['sites'])] + print(session.get('user')['system_admin']) + if not session.get('user')['system_admin']: + return redirect('/logout') + database_config = config() + site_name = session['selected_site'] + with psycopg2.connect(**database_config) as conn: + with conn.cursor() as cur: + sql = f"SELECT id, name FROM {site_name}_zones;" + cur.execute(sql) + zones = cur.fetchall() + return render_template("other/workshop.html", current_site=session['selected_site'], sites=sites, zones=zones) + + +@workshop_api.route('/workshop/getZones', methods=['GET']) +@login_required +def getZones(): + if request.method == "GET": + records = [] + count = 0 + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 10)) + offset = (page - 1) * limit + database_config = config() + site_name = session['selected_site'] + with psycopg2.connect(**database_config) as conn: + records, count = postsqldb.ZonesTable.paginateZones(conn, site_name, (limit, offset)) + return jsonify({'zones': records, "end": math.ceil(count/limit), 'error':False, 'message': 'Zones Loaded Successfully!'}) + return jsonify({'zones': records, "end": math.ceil(count/limit), 'error':True, 'message': 'There was a problem loading Zones!'}) + +@workshop_api.route('/workshop/getLocations', methods=['GET']) +@login_required +def getLocations(): + if request.method == "GET": + records = [] + count = 0 + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 10)) + offset = (page - 1) * limit + database_config = config() + site_name = session['selected_site'] + with psycopg2.connect(**database_config) as conn: + records, count = postsqldb.LocationsTable.paginateLocations(conn, site_name, (limit, offset)) + return jsonify({'locations': records, "end": math.ceil(count/limit), 'error':False, 'message': 'Zones Loaded Successfully!'}) + return jsonify({'locations': records, "end": math.ceil(count/limit), 'error':True, 'message': 'There was a problem loading Zones!'}) + +@workshop_api.route('/workshop/getVendors', methods=['GET']) +@login_required +def getVendors(): + if request.method == "GET": + records = [] + count = 0 + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 10)) + offset = (page - 1) * limit + database_config = config() + site_name = session['selected_site'] + with psycopg2.connect(**database_config) as conn: + records, count = postsqldb.VendorsTable.paginateVendors(conn, site_name, (limit, offset)) + return jsonify({'vendors': records, "end": math.ceil(count/limit), 'error':False, 'message': 'Zones Loaded Successfully!'}) + return jsonify({'vendors': records, "end": math.ceil(count/limit), 'error':True, 'message': 'There was a problem loading Zones!'}) + +@workshop_api.route('/workshop/getBrands', methods=['GET']) +@login_required +def getBrands(): + if request.method == "GET": + records = [] + count = 0 + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 10)) + offset = (page - 1) * limit + database_config = config() + site_name = session['selected_site'] + with psycopg2.connect(**database_config) as conn: + records, count = postsqldb.BrandsTable.paginateBrands(conn, site_name, (limit, offset)) + return jsonify({'brands': records, "end": math.ceil(count/limit), 'error':False, 'message': 'Zones Loaded Successfully!'}) + return jsonify({'brands': records, "end": math.ceil(count/limit), 'error':True, 'message': 'There was a problem loading Zones!'}) + +@workshop_api.route('/workshop/getPrefixes', methods=['GET']) +@login_required +def getPrefixes(): + if request.method == "GET": + records = [] + count = 0 + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 10)) + offset = (page - 1) * limit + database_config = config() + site_name = session['selected_site'] + with psycopg2.connect(**database_config) as conn: + records, count = postsqldb.SKUPrefixTable.paginatePrefixes(conn, site_name, (limit, offset)) + return jsonify({'prefixes': records, "end": math.ceil(count/limit), 'error':False, 'message': 'Zones Loaded Successfully!'}) + return jsonify({'prefixes': records, "end": math.ceil(count/limit), 'error':True, 'message': 'There was a problem loading Zones!'}) + + +@workshop_api.route('/workshop/postAddZone', methods=["POST"]) +def postAddZone(): + if request.method == "POST": + database_config = config() + site_name = session['selected_site'] + try: + with psycopg2.connect(**database_config) as conn: + with conn.cursor() as cur: + cur.execute(f"SELECT id FROM sites WHERE site_name = %s;", (site_name,)) + site_id = cur.fetchone()[0] + zone = postsqldb.ZonesTable.Payload( + request.get_json()['name'], + site_id, + request.get_json()['description'] + ) + postsqldb.ZonesTable.insert_tuple(conn, site_name, zone.payload()) + except Exception as error: + conn.rollback() + return jsonify({'error': True, 'message': error}) + return jsonify({'error': False, 'message': f"Zone added to {site_name}."}) + return jsonify({'error': True, 'message': f"These was an error with adding this Zone to {site_name}."}) + +@workshop_api.route('/workshop/postEditZone', methods=["POST"]) +def postEditZone(): + if request.method == "POST": + database_config = config() + site_name = session['selected_site'] + try: + with psycopg2.connect(**database_config) as conn: + payload = {'id': request.get_json()['zone_id'], + 'update': request.get_json()['update']} + zone = postsqldb.ZonesTable.update_tuple(conn, site_name, payload) + except Exception as error: + conn.rollback() + return jsonify({'error': True, 'message': error}) + return jsonify({'error': False, 'message': f"{zone['name']} edited in site {site_name}."}) + return jsonify({'error': True, 'message': f"These was an error with editing Zone {zone['name']} in {site_name}."}) + +@workshop_api.route('/workshop/postAddLocation', methods=["POST"]) +def postAddLocation(): + if request.method == "POST": + database_config = config() + site_name = session['selected_site'] + try: + with psycopg2.connect(**database_config) as conn: + + location = postsqldb.LocationsTable.Payload( + request.get_json()['uuid'], + request.get_json()['name'], + request.get_json()['zone_id'] + ) + print(request.get_json()) + postsqldb.LocationsTable.insert_tuple(conn, site_name, location.payload()) + except Exception as error: + conn.rollback() + return jsonify({'error': True, 'message': error}) + return jsonify({'error': False, 'message': f"Zone added to {site_name}."}) + return jsonify({'error': True, 'message': f"These was an error with adding this Zone to {site_name}."}) + +@workshop_api.route('/workshop/postAddVendor', methods=["POST"]) +def postAddVendor(): + if request.method == "POST": + database_config = config() + site_name = session['selected_site'] + try: + with psycopg2.connect(**database_config) as conn: + vendor = postsqldb.VendorsTable.Payload( + request.get_json()['vendor_name'], + session['user_id'], + request.get_json()['vendor_address'], + request.get_json()['vendor_phone_number'], + ) + postsqldb.VendorsTable.insert_tuple(conn, site_name, vendor.payload()) + except Exception as error: + conn.rollback() + return jsonify({'error': True, 'message': error}) + return jsonify({'error': False, 'message': f"Zone added to {site_name}."}) + return jsonify({'error': True, 'message': f"These was an error with adding this Zone to {site_name}."}) + +@workshop_api.route('/workshop/postEditVendor', methods=["POST"]) +def postEditVendor(): + if request.method == "POST": + database_config = config() + site_name = session['selected_site'] + try: + with psycopg2.connect(**database_config) as conn: + payload = {'id': request.get_json()['vendor_id'], + 'update': request.get_json()['update']} + vendor = postsqldb.VendorsTable.update_tuple(conn, site_name, payload) + except Exception as error: + conn.rollback() + return jsonify({'error': True, 'message': error}) + return jsonify({'error': False, 'message': f"{vendor['vendor_name']} edited in site {site_name}."}) + return jsonify({'error': True, 'message': f"These was an error with editing Zone {vendor['vendor_name']} in {site_name}."}) + +@workshop_api.route('/workshop/postAddBrand', methods=["POST"]) +def postAddBrand(): + if request.method == "POST": + database_config = config() + site_name = session['selected_site'] + try: + with psycopg2.connect(**database_config) as conn: + brand = postsqldb.BrandsTable.Payload( + request.get_json()['brand_name'] + ) + postsqldb.BrandsTable.insert_tuple(conn, site_name, brand.payload()) + except Exception as error: + conn.rollback() + return jsonify({'error': True, 'message': error}) + return jsonify({'error': False, 'message': f"Brand added to {site_name}."}) + return jsonify({'error': True, 'message': f"These was an error with adding this Zone to {site_name}."}) + +@workshop_api.route('/workshop/postEditBrand', methods=["POST"]) +def postEditBrand(): + if request.method == "POST": + database_config = config() + site_name = session['selected_site'] + try: + with psycopg2.connect(**database_config) as conn: + payload = {'id': request.get_json()['brand_id'], + 'update': request.get_json()['update']} + brand = postsqldb.BrandsTable.update_tuple(conn, site_name, payload) + except Exception as error: + conn.rollback() + return jsonify({'error': True, 'message': error}) + return jsonify({'error': False, 'message': f"{brand['name']} edited in site {site_name}."}) + return jsonify({'error': True, 'message': f"These was an error with editing Zone {brand['name']} in {site_name}."}) + +@workshop_api.route('/workshop/postAddPrefix', methods=["POST"]) +def postAddPrefix(): + if request.method == "POST": + database_config = config() + site_name = session['selected_site'] + try: + with psycopg2.connect(**database_config) as conn: + prefix = postsqldb.SKUPrefixTable.Payload( + request.get_json()['prefix_uuid'], + request.get_json()['prefix_name'], + request.get_json()['prefix_description'] + ) + postsqldb.SKUPrefixTable.insert_tuple(conn, site_name, prefix.payload()) + except Exception as error: + conn.rollback() + return jsonify({'error': True, 'message': error}) + return jsonify({'error': False, 'message': f"Prefix added to {site_name}."}) + return jsonify({'error': True, 'message': f"These was an error with adding this Prefix to {site_name}."}) + +@workshop_api.route('/workshop/postEditPrefix', methods=["POST"]) +def postEditPrefix(): + if request.method == "POST": + database_config = config() + site_name = session['selected_site'] + try: + with psycopg2.connect(**database_config) as conn: + payload = {'id': request.get_json()['prefix_id'], + 'update': request.get_json()['update']} + prefix = postsqldb.SKUPrefixTable.update_tuple(conn, site_name, payload) + except Exception as error: + conn.rollback() + return jsonify({'error': True, 'message': error}) + return jsonify({'error': False, 'message': f"{prefix['name']} edited in site {site_name}."}) + return jsonify({'error': True, 'message': f"These was an error with editing Zone {prefix['name']} in {site_name}."})