From 1156ab5aca6948ad01e2385de437861e1f87130d Mon Sep 17 00:00:00 2001 From: Jadowyne Ulve Date: Fri, 22 Aug 2025 07:08:17 -0500 Subject: [PATCH] Added Take Out Events and REceipts --- .../database_payloads.cpython-313.pyc | Bin 28829 -> 28895 bytes .../administration/sql/CREATE/plan_events.sql | 1 + application/database_payloads.py | 2 + .../meal_planner_api.cpython-313.pyc | Bin 7462 -> 9160 bytes .../meal_planner_database.cpython-313.pyc | Bin 8869 -> 14331 bytes .../meal_planner_processes.cpython-313.pyc | Bin 0 -> 2597 bytes application/meal_planner/meal_planner_api.py | 35 ++- .../meal_planner/meal_planner_database.py | 129 +++++++++- .../meal_planner/meal_planner_processes.py | 66 +++++ .../meal_planner/sql/insertPlanEvent.sql | 4 +- .../sql/insertReceiptItemsTuple.sql | 4 + .../meal_planner/sql/insertReceiptsTuple.sql | 4 + .../meal_planner/static/css/planner.css | 18 ++ .../static/js/mealPlannerHandler.js | 231 ++++++++++++++++++ .../meal_planner/templates/meal_planner.html | 97 ++++++++ logs/database.log | 5 +- 16 files changed, 591 insertions(+), 5 deletions(-) create mode 100644 application/meal_planner/__pycache__/meal_planner_processes.cpython-313.pyc create mode 100644 application/meal_planner/sql/insertReceiptItemsTuple.sql create mode 100644 application/meal_planner/sql/insertReceiptsTuple.sql diff --git a/application/__pycache__/database_payloads.cpython-313.pyc b/application/__pycache__/database_payloads.cpython-313.pyc index af1b7f0fd5aebe7e5bdda7f8e1726a8eef5ff3eb..278919b291d7d068c3e53c641cfe8f418b2b420b 100644 GIT binary patch delta 429 zcmWmAJxD@P6bJBo?P>aa&m>b+(50xXhM|K~wh}I6n@Y|8mYngMKuqr76oI;q@E4PTPa`mVJ$00uA(n zyHA9Q7IVghxLy@|ctkb32{Wn^Go@so1-g^FMu}-#S|(qF^Q2ca@k|Tv<|9;x3Q+AG zfIo`^&ZZsaVN(}jZG7i5K`G_ajZoO=x4}t=9%?CENr%SaSiMPkSrvd+2Du`x;n!p89+Cwy%^?b2&vnj))4U*#+u{uj~W&D(9h_ Dqoi&- delta 427 zcmccrka6xqM!wIyyj%=GkTr2$#yy9Pe08aeQzkRoYD|{W;+niaHIgrwONyb0Bbd90 zGo441cd|iRwGvlqS!!NMd`V?NYBD28GZb(DX=Wh)ylwIUC{n-6E6XH0Sj zy5<&ZW=U#paSjBn!8sXEGPz21oo2|?!ZKF&6VCUW@(#qTM~Gw zG0tI(4{#pl!?HLZ%zRr?eKFDaX!c+RGhwMqe9%9j1Rr$q!SDCnv9=4SC+Vm6e9!lM zzMu0w-|zYIl^@T$cAZYU0mhNH?_Ki*)?I#K#JC0?QcvqHjr6Qi*IzNvBns$kNQbs%ffL z{K)twAtAPHijmNUIZfJ&8S`Jn;s;_Xv9#4k!t93iL2m~dIuVWlXeNLN4B7@CrG^DH zcXnA#=d?2`7c%MGHQHA!*dCLZPJ=FX71tB_{9>|XpiwZ<7{F>X-^Y4gVxN)5(R>)) z&D@=Kq2VY%+(t1Z?MAR7^dcN%5A4ySC-8FsVGuww0z8-I+LLx>WqNx2bvnc(#|Nap zc-Ju|G*;x9vdV%=!|bWE#eEW)1O!z0l*L@FBNb&?Aj312 zH(e(#a+RmhID#+=P_ksMUe(kbJ%bbTd?pVm#M6O%9C~)`aD*v6C>@7I?5o|P&Fv;l zSm~?o9wocj15PubC_{^1JNkvf^P^E7G7OG-+|&3qicot_`YPDsrm8!z!i@o+Mqfkk z%2oW^Nztf^i?H__kBD|NtjdnTy0mc? zRr4|BVU12N%X{*cNiaYcls(IZx>6lB4=-RUqx~|%C4?z}l6{`4aN1!Grs_-Qx$<f90@oV>N^Vr5yr)j)^=E}wP29+9rDRGfIfZlAcDh}c?}n<)(Q81H zf}eJs-4&&%%e(E8wp`L9S7gof#1r@|zUg^s&HB4d+O~CX*}Cto{9ygrHpc!G}#@IM|Ck~7h78Y^B`mkPjt^XtBCxOn&L#W~NwfBBFsJfKd3zvn3oAA?ANK7wt1qUry0r7TrA$Z!t z8}l8u5xml<--IoZNGnTfB0)Xu?_l$#cJQTr2tx??9Ptk1b;q+#@jw*A=&wAo>+hIt zcqx<2r_>R;0yY9atw&&2n!Aj@8m8E%%{xS5W1*J;3!%sbFIgbFMuX6dFjO|-yxeUN vJljOvB;rGV{QkN7Cm+HW8GcM0+a$P2f;&Q8^|n)G6VA(i_DyKXgi8Ja6@tq# delta 1520 zcma)5O>7%g5Z=;n452A)AjECcP^jD%3RO%%sDbU)`{EkxwR!7} z(0!aRY>qR^?cU_LL|{RWFE{HMSdb;Ys`L>^Jk? zd^0=m$HM3N&|Wa;6JUK@yMDPnvK5MppNdC%SH~kN|OLew;+yV?Dmo<39vG+2w;HgU9jTz(3ye zj~1i*(LP2jh}2itFXZ;*+879YiE z)v_wOEwC8~WLci`PaU2`GY4S1oe=gI5_14KnPF1)I0DEM*gV1^{*FJpco@y65S|7w zT>w3Cp{}@-t+ll?&#^o>u^Q7tX$pVfzd#n+Yk||^a9^O*(8?4_|2mWkFG4UA5wZYR z`0Y?;xvz7G(Nd*BP1{8c{UN#jLAeLY(R>Wa;|M1JY)^e-!=z2Nf*Z!+#i|t`GLR|?0v%D4dkQ((Uf*M@)hBC zMss{QDifI(qSrj9A%rRXPIU9CYi%US%l{SY+s2{tx0wDQj5KW?0E#I z9|`Brr!`%tdb((pO4KwrtZKED`BG%PK0VZ#z(8kgng1D6SDb$IGX^^&($HGfx~2~d zoDA8VwCn=tcm^JGgufsEVeEEn;`dngmss}KSni9tpJR)=!`_7NgWz9Lt|eCBKW`_` zkm*k4SOLzA?XengLugL4C7ek1I(o8vUP)z&X!d7tXh1r9h1Zptx&HX!*8St>HMLFJ z(RvY_ukmf=J-*PbwElZmH8hjze&x^_EMTSncdOwGg_;6)0M!n!T(Mfo&~US zY0lCoJkNs3UgqDX-?(bas%mI8s;X=lSli9&R+HMHYQ0=BR7T&nDvauE5_jNzc5aR{ zaZcCSMdUC^=K^Dr&Vb6tYIWVJ(q&c!j=*DX1Mav8A@?Pbc+nHT5 f&hJg8qY+XjdoDptA{^b3@Ox*DzdBWu@brHGydGcq diff --git a/application/meal_planner/__pycache__/meal_planner_database.cpython-313.pyc b/application/meal_planner/__pycache__/meal_planner_database.cpython-313.pyc index e1e41040d56bfbd94fab6eed3a0805304f3c03bd..81b57f1a60e6b063e0697ed93973997388c78051 100644 GIT binary patch delta 4006 zcmc&%T}&Iv9pCZp`fL5gR}2QjVgeUKAmKvN1PBRWLues@7e{Hyi8^K>PR$x-mo(wU zNPX!;P6bV?OBJ=HQmeb1PHIwKuBue!8lqlLl?owM=pL%>p;sv{RZ?;nT~ew2&)SBN zkhIsA4)W~G@Bg2f|Mv_(4?j)0Hk?j71MRP;-#K?SIPWS(f0%zQkakO~k2#MpCwV09 z3@`C#%uSqR28;vLvJJIlQR_C;nni8fP+JzYZ$s@_Q~*?fF-l_x3>|ccotvyK5W$&c zbpz@$=$>&D%)3u@I2lB`Z8D{5>Qr*{Y}`b#Y_q_Zh2O?Hpg&+Fq?ja=!b)tEaWN8i zfK|A1CW`iC*~#mu9`WRxsItPjpNaAzbOPiEc8UV zgD683nW#D9BvVdbS}{dU5^6rpFfmvwnH5w21Rxe3I2F^dJF|dcqNYe*ZoKuCthp^E z8(pzAtA`n^FUavmO*xuk87|sH)0R!xD%m669AWD_T#_40LeyFh;gDNVz?`zFrhSZR zDqv2)>55Hq44C@Bm;xgLs@QUziv9H48G$53{|;u2FEXAeo3m<61T7J1FRx%x#80MN ze!;XGw#jTo=4}4%)}w3%GlnXe;qshN!<&AL!20J~lgYxwNVGnMeUp{D)L25OpJqCOuO^uzC7qkTqIXN~$Y1m2SaF52c*x8sWQ*10T zuCw3;9bM8baNr0%8I9=ZqRtz}Q5~>`8b8ts=h?%F}lp zWozY?KS};Dc~$vt|C+meVfU)LW-YLH-u+3S@p|P-plQy%9;jYO-K%L?scCt?bG7F9 z&AgSG&ecHZ=G02y1dt28SKO?MU$(YCeZW%M6SWhJ zak*Ux9b!K4cA!u-d#l(H679Ek3v|41KMz=6_Rs;#(mo4C4*9|*b?j}=u5b-| zyM_+g*J3sAc5rBnz>6mV;TOcSd$XLT7g=og>76|^tq^#K9$*d zXnHa!tGEr;^pR_PfQv5MPc8nD=Maxu0bKN1EUcNzHFKBYp>Zv# z7yNuT;K{{)VI`vgZXh-8`u`xj5h1j?yj*zU<-!Zk2>-m)hR{KV+;JCF(lF4Apft&G zZ6Fy_!c^yuOVKD)kgo#m@}k-EFBS>JTzDvboJ!LU-O#l>+(9Qar*MdNulB@F%D`pz zf;${9)d=@6;6L0&7f~}mM$iq0eS5oxrXBR|SJPd+K* zO033vhn37xHc4xx(ctC66VW>52A4)nn|FGE{4Bqd=lVTSJ85*4+UyD^G4zjoqqvj= z3);wHak=I1nQNEKyGqI11!t^AB6S%NTb0fgXHA|^jc{*(?8V37)QnYeFARe_+CXay)ZfETbr|=>T0zlluW?(f+nmdu6CXC+wGAu} zohFS%W!xZoz>)8XA#zVBs$8$wbFbpqor+^O+26T8a4!!=S1V4?lrCFKAJ;MVLG&lvz>~-M$0GB&NkbL&DZ z3D28s*4KbY#w~}p(tfA1`>xMtegk+BTKve@z=7~zlz%6UOX%`q+P%79?R0hQq|DCA zY8i!snp5W4az(FaLvS78%$2c2$r44M`7AJsXz{o>jne33&@^;1m}fjGbRjkgkJETe zOQe*{}8)1&jojfLsGZrrAr-abY#$FvX{iEhM+&W$GG8m3ml^gN?hZNPvr zUI)6hbs{x7ootrd@Hl`FP>lK(bnu`;=wrtHDO33|<9x)`pbqqK0QphN0{ei0&pYyG m^z*Qd91VQ!wMT6})bi*sbPo>$dL^FW2^l<}Q6e6LUH=UeXMnc= delta 1468 zcmb7EOKTHR6rP*RBgrJGt>!_hHi^_49~H≫K51DhMK`LZQVvPOf#rbY{FWsSkvR zu5>3?L=c27M7o|o;nwYS<3_=aSh{qf=OoeC5^Eipk2~MJIrn_$d?(-CH&f~LR4O6C zGd%n1)@1%!I!|6c8!0#SYII(%%Ja$zxvCVTx?Cqrt^UcX9ah`GY8^JVfsJ+8_-{4= zapN6U2Q~p~!jg-mlKcYM?3FH6h=_ZxANaTJ+6^K5RzQX38TV}0sD&3}?;>QC=k#fE zpI_6{mK+A;b;MUpcy|crMb-GNQU?IR7v0j;$Bv9#NS&H3$1Or@cr|cs zjpgAdBH`0u!L*HrPldW{*bT67TT{!9T`RR*RBDT8N7y9d6oh*ug`ba(${-+f;&D^R zb=Pu4)TYiq+S1GVy%hEAiO_1)roR+*J2KoI1VI=pL4G~D!cUDA)N`=6B878f4bqIv zT!J~nDHXcmdA4O5f#o`F@1wime9bT#J xf!OIKx7M)f1iJtnf!Fr|zC{TkAEbESC#m#7O0CKK$@s;{+64bNo|DhRfuAgSE9U?J diff --git a/application/meal_planner/__pycache__/meal_planner_processes.cpython-313.pyc b/application/meal_planner/__pycache__/meal_planner_processes.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8bfe9668304dfda697f3644b97a698b0e7df5fe0 GIT binary patch literal 2597 zcmbVOO-vg{6yCMh|7)*}Z5A8|gkXrP00{|3B!$ung7_(kyjW>u8h7zdh#PyonO#RA zhsuW>h!m;GArfj2?J>P^%&k4<=rxtRnxHgN=^?j9r1#FOz1T!OL>=LIKlA3j@4b05 z%c0-zrSMqJJijfuDe6xmSi{z24!^+6GfJQYdYOU^N<$jUwq@Ir9oqYe&c4hnIiO?7 z37w`)FT0>ia0pC{`j&?7O*-LxOB}8yXcR31v5-{?qAUZLl|)V45LJ+{A&*j0HT7;m z+CU*oomIrGLP?a=9Mc3u2#bGq{5$*(&(Ej~(U~JN$OEI$}&TA&?RC)g&MiOS6X z@?!tMivxRRh)#A24%wBqpJ_&xE>cwHD2LP$*8(;`O*94BopD_#{eaCpQF<5 zIXX$pcEORh7f48R2u|6RA@wmwDqwLfIzWA87hF>|*^>!c+Ej1)JbW z+tN@Wib37R7jcX`jHF z7557iu5^I#m~y5rh`Er-IfRZ;n-HFI%WNCoo|Z3*!+6<`z{|GbW&acJNy}r4V!Z7b zBPLcR?ZCC9g{Ud{gie#v0&n8(n)1m0HpKlcZx)9U`#%D4pbc@Lg*ff9`cXWED`!ug zw*0oJbyodkJ>Jq=Tx)&PvFS|2R#6b#1F}Z0GRUf$2(<_9_>GZxaU?FGK-(ISCFH@T zQVC}B(v>y*7%;h#EI(k@uBJwiH!VY;mJ0WPlwgn-f*i;z8n#3Mi^txzvWi%>yiv?+ z8j!LZThN1%A!n|vYNaBwVRH&`ji~DF64Yd|2#`m_*0KbE`j)gCiK}V}8;lac1@0?( z96I4fjJBl!NbV1EZHO>elJMTV28!8o8GB|-XZE|=7P6OdG={uWDAddms5yuc=Swp3 zVZ?a_GRxf zP_iij-_oM!Rq9qKECvFX^+2NWg`30-g*ub#^zKE4%Ice7y`959~k;fJdS;lX-% zvKF2+INk`yb}l}hHi8{SN2lQrRXa}W{y~EcJ~^SYy$0(yI%7M@r-kQb-9Pazdc?|qMakNt%4KJq?h4KDJ=#~tExq!RRM z!UM^Z3&}H$y4t#Aw7p9jm)F;}49Pzf=Ea4>gqtPPAelZV(`R@>?oW#)sayava1M*) i8C1z<_-{K+)2}EF|NTW>`h&Xoit_)%T%v>K)_(!uEOA`` literal 0 HcmV?d00001 diff --git a/application/meal_planner/meal_planner_api.py b/application/meal_planner/meal_planner_api.py index e311b31..c5d6163 100644 --- a/application/meal_planner/meal_planner_api.py +++ b/application/meal_planner/meal_planner_api.py @@ -10,7 +10,7 @@ import datetime from config import config from application.access_module import access_api from application import postsqldb, database_payloads -from application.meal_planner import meal_planner_database +from application.meal_planner import meal_planner_database, meal_planner_processes meal_planner_api = Blueprint('meal_planner_api', __name__, template_folder="templates", static_folder="static") @@ -62,6 +62,24 @@ def getRecipes(): return jsonify(status=201, message="Recipes fetched Successfully!", recipes=recipes, end=math.ceil(count/limit)) return jsonify(status=405, message=f"{request.method} is not an allowed method on this endpoint!", recipes=recipes, end=math.ceil(count/limit)) + +@meal_planner_api.route('/api/getVendors', methods=["GET"]) +@access_api.login_required +def getVendors(): + if request.method == "GET": + site_name = session['selected_site'] + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 50)) + search_string = request.args.get('search_string', "") + + offset = (page - 1) * limit + vendors, count = [], 0 + vendors, count = meal_planner_database.paginateVendorsTuples(site_name, (limit, offset)) + + return jsonify(status=201, message="Recipes fetched Successfully!", vendors=vendors, end=math.ceil(count/limit)) + return jsonify(status=405, message=f"{request.method} is not an allowed method on this endpoint!", vendors=vendors, end=math.ceil(count/limit)) + + @meal_planner_api.route('/api/addEvent', methods=["POST"]) @access_api.login_required def addEvent(): @@ -78,6 +96,7 @@ def addEvent(): event_date_end=event_date_end, created_by=session['user_id'], recipe_uuid=request.get_json()['recipe_uuid'], + receipt_uuid=None, event_type=request.get_json()['event_type'] ) @@ -86,6 +105,20 @@ def addEvent(): return jsonify(status=201, message="Event added Successfully!") return jsonify(status=405, message=f"{request.method} is not an allowed method on this endpoint!") +@meal_planner_api.route('/api/addTOEvent', methods=["POST"]) +@access_api.login_required +def addTOEvent(): + if request.method == "POST": + site_name = session['selected_site'] + data= request.get_json() + user_id = session['user_id'] + + meal_planner_processes.addTakeOutEvent(site_name, data, user_id) + + return jsonify(status=201, message="Event added Successfully!") + return jsonify(status=405, message=f"{request.method} is not an allowed method on this endpoint!") + + @meal_planner_api.route('/api/saveEvent', methods=["POST"]) @access_api.login_required def saveEvent(): diff --git a/application/meal_planner/meal_planner_database.py b/application/meal_planner/meal_planner_database.py index a7037a0..6b2a9b1 100644 --- a/application/meal_planner/meal_planner_database.py +++ b/application/meal_planner/meal_planner_database.py @@ -3,6 +3,46 @@ import psycopg2 from application import postsqldb import config +def requestNextReceiptID(site_name, conn=None): + """gets the next id for receipts_id, currently returns a 8 digit number + + Args: + site (str): site to get the next id for + + Returns: + json: receipt_id, message, error keys + """ + next_receipt_id = None + self_conn = False + sql = f"SELECT receipt_id FROM {site_name}_receipts ORDER BY id DESC LIMIT 1;" + try: + if not conn: + database_config = config.config() + conn = psycopg2.connect(**database_config) + conn.autocommit = True + self_conn = True + + with conn.cursor() as cur: + cur.execute(sql) + next_receipt_id = cur.fetchone() + if next_receipt_id == None: + next_receipt_id = "00000001" + else: + next_receipt_id = next_receipt_id[0] + next_receipt_id = int(next_receipt_id.split("-")[1]) + 1 + y = str(next_receipt_id) + len_str = len(y) + x = "".join(["0" for _ in range(8 - len_str)]) + next_receipt_id = x + y + + if self_conn: + conn.commit() + conn.close() + + return next_receipt_id + except (Exception, psycopg2.DatabaseError) as error: + raise postsqldb.DatabaseError(error, payload=(), sql=sql) + def paginateRecipesTuples(site: str, payload: tuple, convert=True, conn=None): self_conn = False recipes = () @@ -34,6 +74,37 @@ def paginateRecipesTuples(site: str, payload: tuple, convert=True, conn=None): except Exception as error: raise postsqldb.DatabaseError(error, payload, sql) +def paginateVendorsTuples(site: str, payload: tuple, convert=True, conn=None): + self_conn = False + recipes = () + count = 0 + sql = f"SELECT * FROM {site}_vendors ORDER BY vendor_name ASC LIMIT %s OFFSET %s;" + sql_count = f"SELECT COUNT(*) FROM {site}_vendors;" + try: + if not conn: + database_config = config.config() + conn = psycopg2.connect(**database_config) + conn.autocommit = True + self_conn = True + + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + recipes = [postsqldb.tupleDictionaryFactory(cur.description, row) for row in rows] + if rows and not convert: + recipes = rows + + cur.execute(sql_count) + count = cur.fetchone()[0] + + if self_conn: + conn.close() + + return recipes, count + except Exception as error: + raise postsqldb.DatabaseError(error, payload, sql) + def selectPlanEventsByMonth(site: str, payload: tuple, convert=True, conn=None): """payload=(year, month)""" self_conn = False @@ -126,7 +197,63 @@ def insertPlanEventTuple(site: str, payload: tuple, convert=True, conn=None): return event_tuple except Exception as error: raise postsqldb.DatabaseError(error, payload, sql) - + +def insertReceiptItemsTuple(site, payload, convert=True, conn=None): + receipt_item = () + self_conn = False + with open(f"application/meal_planner/sql/insertReceiptItemsTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + if not conn: + database_config = config.config() + conn = psycopg2.connect(**database_config) + conn.autocommit = True + self_conn = True + + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + receipt_item = postsqldb.tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + receipt_item = rows + + if self_conn: + conn.commit() + conn.close() + + return receipt_item + except Exception as error: + raise postsqldb.DatabaseError(error, payload, sql) + +def insertReceiptsTuple(site, payload, convert=True, conn=None): + receipt = () + self_conn = False + with open(f"application/meal_planner/sql/insertReceiptsTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + if not conn: + database_config = config.config() + conn = psycopg2.connect(**database_config) + conn.autocommit = True + self_conn = True + + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + receipt = postsqldb.tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + receipt = rows + + if self_conn: + conn.commit() + conn.close() + + return receipt + except Exception as error: + raise postsqldb.DatabaseError(error, payload, sql) + def updatePlanEventTuple(site:str, payload: dict, convert=True, conn=None): """ payload (dict): {'barcode': row_id, 'update': {... column_to_update: value_to_update_to...}} """ updated = () diff --git a/application/meal_planner/meal_planner_processes.py b/application/meal_planner/meal_planner_processes.py index e69de29..460614c 100644 --- a/application/meal_planner/meal_planner_processes.py +++ b/application/meal_planner/meal_planner_processes.py @@ -0,0 +1,66 @@ +import datetime +import psycopg2 + +from application.meal_planner import meal_planner_database +from application import postsqldb, database_payloads +import config + +def addTakeOutEvent(site, data, user_id, conn=None): + event_date_start = datetime.datetime.strptime(data['event_date_start'], "%Y-%m-%d") + event_date_end = datetime.datetime.strptime(data['event_date_end'], "%Y-%m-%d") + + vendor_id = data['vendor_id'] + + self_conn = False + if not conn: + database_config = config.config() + conn = psycopg2.connect(**database_config) + conn.autocommit = False + self_conn = True + + receipt_id = meal_planner_database.requestNextReceiptID(site, conn=conn) + + receipt_payload = database_payloads.ReceiptPayload( + receipt_id=f"TOR-{receipt_id}", + receipt_status="Unresolved", + submitted_by=user_id, + vendor_id=vendor_id + ) + + receipt = meal_planner_database.insertReceiptsTuple(site, receipt_payload.payload(), conn=conn) + + print(receipt) + + receipt_item = database_payloads.ReceiptItemPayload( + type = 'custom', + receipt_id=receipt['id'], + barcode="", + item_uuid=None, + name=data['event_shortname'], + qty=data['attendees'], + uom=1, + data={'cost': data['cost'], 'expires': False} + ) + + receipt_item = meal_planner_database.insertReceiptItemsTuple(site, receipt_item.payload(), conn=conn) + print(receipt_item) + event_payload = database_payloads.PlanEventPayload( + plan_uuid=None, + event_shortname=data['event_shortname'], + event_description=data['event_description'], + event_date_start=event_date_start, + event_date_end=event_date_end, + created_by=user_id, + recipe_uuid=data['recipe_uuid'], + receipt_uuid=receipt['receipt_uuid'], + event_type=data['event_type'] + ) + + event = meal_planner_database.insertPlanEventTuple(site, event_payload.payload(), conn=conn) + print(event) + if self_conn: + conn.commit() + conn.close() + return False + + return True \ No newline at end of file diff --git a/application/meal_planner/sql/insertPlanEvent.sql b/application/meal_planner/sql/insertPlanEvent.sql index 6902fa8..96dcd92 100644 --- a/application/meal_planner/sql/insertPlanEvent.sql +++ b/application/meal_planner/sql/insertPlanEvent.sql @@ -1,4 +1,4 @@ INSERT INTO %%site_name%%_plan_events -(plan_uuid, event_shortname, event_description, event_date_start, event_date_end, created_by, recipe_uuid, event_type) -VALUES (%s, %s, %s, %s, %s, %s, %s, %s) +(plan_uuid, event_shortname, event_description, event_date_start, event_date_end, created_by, recipe_uuid, receipt_uuid, event_type) +VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING *; \ No newline at end of file diff --git a/application/meal_planner/sql/insertReceiptItemsTuple.sql b/application/meal_planner/sql/insertReceiptItemsTuple.sql new file mode 100644 index 0000000..8f06d55 --- /dev/null +++ b/application/meal_planner/sql/insertReceiptItemsTuple.sql @@ -0,0 +1,4 @@ +INSERT INTO %%site_name%%_receipt_items +(type, receipt_id, barcode, item_uuid, name, qty, uom, data, status) +VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) +RETURNING *; \ No newline at end of file diff --git a/application/meal_planner/sql/insertReceiptsTuple.sql b/application/meal_planner/sql/insertReceiptsTuple.sql new file mode 100644 index 0000000..8ddaf60 --- /dev/null +++ b/application/meal_planner/sql/insertReceiptsTuple.sql @@ -0,0 +1,4 @@ +INSERT INTO %%site_name%%_receipts +(receipt_id, receipt_status, date_submitted, submitted_by, vendor_id, files) +VALUES (%s, %s, %s, %s, %s, %s) +RETURNING *; \ No newline at end of file diff --git a/application/meal_planner/static/css/planner.css b/application/meal_planner/static/css/planner.css index 576ac2b..258199a 100644 --- a/application/meal_planner/static/css/planner.css +++ b/application/meal_planner/static/css/planner.css @@ -133,6 +133,24 @@ background-color: rgb(255, 255, 255); } +.take-out-label { + background:rgb(250, 162, 238); + margin-bottom: 3px; + padding: 2px 5px; + border-radius: 1px; + font-size: 12px; + font-weight: bold; +} + +.take-out-label:hover{ + background-color: rgb(225, 255, 255); + cursor: pointer; +} + +.take-out-label:hover, .custom-label-selected { + background-color: rgb(255, 255, 255); +} + .my-list-item:hover { background-color: whitesmoke; } diff --git a/application/meal_planner/static/js/mealPlannerHandler.js b/application/meal_planner/static/js/mealPlannerHandler.js index dccf52f..25f1b2a 100644 --- a/application/meal_planner/static/js/mealPlannerHandler.js +++ b/application/meal_planner/static/js/mealPlannerHandler.js @@ -105,6 +105,7 @@ async function setupCalendarAndEvents(){ let recipeLabel = e.target.closest('.recipe-label'); let calendarCell = e.target.closest('.calendar-cell'); let customLabel = e.target.closest('.custom-label'); + let takeOutLabel = e.target.closest('.take-out-label') if (recipeLabel) { recipeLabel.classList.add('recipe-label-selected') let rect = recipeLabel.getBoundingClientRect(); @@ -121,6 +122,14 @@ async function setupCalendarAndEvents(){ let menuX = rect.left + scrollLeft; let menuY = rect.bottom + scrollTop; showContextMenuForEvent(customLabel, menuX, menuY); + } else if (takeOutLabel) { + takeOutLabel.classList.add('take-out-label-selected') + let rect = takeOutLabel.getBoundingClientRect(); + let scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; + let scrollTop = window.pageYOffset || document.documentElement.scrollTop; + let menuX = rect.left + scrollLeft; + let menuY = rect.bottom + scrollTop; + showContextMenuForTOEvent(takeOutLabel, menuX, menuY); } else if (calendarCell) { calendarCell.classList.add('calendar-cell-selected') let rect = calendarCell.getBoundingClientRect(); @@ -167,6 +176,8 @@ async function createCalender() { eventsHTML += `
${event.event_shortname}
`; } else if (event.event_type==="recipe" && !event.has_missing_ingredients){ eventsHTML += `
${event.event_shortname}
`; + } else if (event.event_type==="take out"){ + eventsHTML += `
${event.event_shortname}
`; } else { eventsHTML += `
${event.event_shortname}
`; } @@ -218,6 +229,21 @@ function showContextMenuForEvent(eventLabel, x, y) { menu.style.top = y + 'px'; } +function showContextMenuForTOEvent(eventLabel, x, y) { + const menu = document.getElementById('calendarContextMenu'); + // Set only "Edit" and "Remove" (and optionally "Add Another") + menu.className = "uk-dropdown uk-open"; + menu.innerHTML = ` + + `; + menu.style.display = 'block'; + menu.style.left = x + 'px'; + menu.style.top = y + 'px'; +} + + function showContextMenuForCell(calendarCell, x, y) { const menu = document.getElementById('calendarContextMenu'); // Only "Add Event" @@ -225,6 +251,7 @@ function showContextMenuForCell(calendarCell, x, y) { menu.innerHTML = ` `; menu.style.display = 'block'; @@ -237,6 +264,7 @@ window.addEventListener('click', function() { document.querySelectorAll('.calendar-cell-selected').forEach(el => el.classList.remove('calendar-cell-selected')); document.querySelectorAll('.custom-label-selected').forEach(el => el.classList.remove('custom-label-selected')); document.querySelectorAll('.recipe-label-selected').forEach(el => el.classList.remove('recipe-label-selected')); + document.querySelectorAll('.take-out-label-selected').forEach(el => el.classList.remove('recipe-label-selected')); }); async function addEvent(day) { @@ -593,4 +621,207 @@ async function updateEventsPaginationElement() { console.log(nextElement.innerHTML) } paginationElement.append(nextElement) +} + +// Take Out Event Functions +var TO_current_page = 1; +var TO_end_page = 1; +var TO_Limit = 10; +var TO_search_string = ""; + +async function addTakeOut(day) { + TO_current_page = 1; + TO_end_page = 1; + TO_Limit = 10; + TO_search_string = ""; + let menu = document.getElementById('calendarContextMenu'); + //let day = menu.getAttribute('data-day') + console.log(year, month, day) + let customDate = new Date(year, month-1, day); + document.getElementById('TO_date_start').value = customDate.toISOString().split('T')[0]; + document.getElementById('TO_date_end').value = customDate.toISOString().split('T')[0]; + UIkit.modal(document.getElementById('takeOutOrderModal')).show(); +} + +async function selectTOEvent() { + document.getElementById('TOModalBody').hidden = true + document.getElementById('paginationTOModalBody').hidden = false + document.getElementById('TOModalFooter').hidden = true + let vendors = await fetchVendors() + await updateTOPaginationElement() + await updateTOTableWithVendors(vendors) +} + +async function fetchVendors() { + const url = new URL('/planner/api/getVendors', window.location.origin); + url.searchParams.append('page', TO_current_page); + url.searchParams.append('limit', TO_Limit); + url.searchParams.append('search_string', TO_search_string); + const response = await fetch(url); + data = await response.json(); + TO_end_page = data.end + return data.vendors; +} + +async function updateTOTableWithVendors(vendors) { + let vendorsTableBody = document.getElementById('vendorsTableBody') + vendorsTableBody.innerHTML = "" + + + for (let i = 0; i < vendors.length; i++){ + let tableRow = document.createElement('tr') + + let nameCell = document.createElement('td') + nameCell.innerHTML = `${vendors[i].vendor_name}` + + + let opCell = document.createElement('td') + + let selectButton = document.createElement('button') + selectButton.setAttribute('class', 'uk-button uk-button-primary uk-button-small') + selectButton.innerHTML = "Select" + selectButton.onclick = async function() { + document.getElementById('vendor_id').value = vendors[i].id + document.getElementById('selected_vendor_name').value = vendors[i].vendor_name + document.getElementById('TOModalBody').hidden = false + document.getElementById('paginationTOModalBody').hidden = true + document.getElementById('TOModalFooter').hidden = false + } + + opCell.append(selectButton) + + tableRow.append(nameCell, opCell) + vendorsTableBody.append(tableRow) + } +} + +async function setTOModalPage(pageNumber){ + TO_current_page = pageNumber; + let vendors = await fetchVendors() + await updateTOTableWithVendors(vendors) + await updateTOPaginationElement() +} + +async function updateTOPaginationElement() { + let paginationElement = document.getElementById('takeOutOrderPage'); + paginationElement.innerHTML = ""; + // previous + let previousElement = document.createElement('li') + if(TO_current_page<=1){ + previousElement.innerHTML = ``; + previousElement.classList.add('uk-disabled'); + }else { + previousElement.innerHTML = ``; + } + paginationElement.append(previousElement) + + //first + let firstElement = document.createElement('li') + if(TO_current_page<=1){ + firstElement.innerHTML = `1`; + firstElement.classList.add('uk-disabled'); + }else { + firstElement.innerHTML = `1`; + } + paginationElement.append(firstElement) + + // ... + if(TO_current_page-2>1){ + let firstDotElement = document.createElement('li') + firstDotElement.classList.add('uk-disabled') + firstDotElement.innerHTML = ``; + paginationElement.append(firstDotElement) + } + // last + if(TO_current_page-2>0){ + let lastElement = document.createElement('li') + lastElement.innerHTML = `${TO_current_page-1}` + paginationElement.append(lastElement) + } + // current + if(TO_current_page!=1 && TO_current_page != TO_end_page){ + let currentElement = document.createElement('li') + currentElement.innerHTML = `
  • ${TO_current_page}
  • ` + paginationElement.append(currentElement) + } + // next + if(TO_current_page+2${TO_current_page+1}` + paginationElement.append(nextElement) + } + // ... + if(TO_current_page+2<=TO_end_page){ + let secondDotElement = document.createElement('li') + secondDotElement.classList.add('uk-disabled') + secondDotElement.innerHTML = ``; + paginationElement.append(secondDotElement) + } + //end + let endElement = document.createElement('li') + if(TO_current_page>=TO_end_page){ + endElement.innerHTML = `${TO_end_page}`; + endElement.classList.add('uk-disabled'); + }else { + endElement.innerHTML = `${TO_end_page}`; + } + paginationElement.append(endElement) + //next button + let nextElement = document.createElement('li') + if(TO_current_page>=TO_end_page){ + nextElement.innerHTML = ``; + nextElement.classList.add('uk-disabled'); + }else { + nextElement.innerHTML = ``; + console.log(nextElement.innerHTML) + } + paginationElement.append(nextElement) +} + +async function postTOEvent(){ + let event_shortname = `Take Out: ${document.getElementById('selected_vendor_name').value}` + let event_description = `Take out dining at ${event_shortname}` + let event_date_start = document.getElementById('TO_date_start').value + let event_date_end = document.getElementById('TO_date_end').value + let event_type = 'take out' + let recipe_uuid = null + + let vendor_id = parseInt(document.getElementById('vendor_id').value) + let attendees = parseInt(document.getElementById('TO_attendees').value) + let cost = parseFloat(document.getElementById('TO_cost').value) + + const response = await fetch('/planner/api/addTOEvent', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + event_shortname: event_shortname, + event_description: event_description, + event_date_start: event_date_start, + event_date_end: event_date_end, + recipe_uuid: recipe_uuid, + event_type: event_type, + vendor_id: vendor_id, + attendees: attendees, + cost: cost + }) + }); + + data = await response.json(); + response_status = 'primary' + if (!data.status === 201){ + response_status = 'danger' + } + + UIkit.notification({ + message: data.message, + status: response_status, + pos: 'top-right', + timeout: 5000 + }); + + await setupCalendarAndEvents() + UIkit.modal(document.getElementById('takeOutOrderModal')).hide(); + } \ No newline at end of file diff --git a/application/meal_planner/templates/meal_planner.html b/application/meal_planner/templates/meal_planner.html index d6a5f8f..9ef1a1b 100644 --- a/application/meal_planner/templates/meal_planner.html +++ b/application/meal_planner/templates/meal_planner.html @@ -203,6 +203,103 @@ + +
    +
    + +
    +

    Take Out Form

    +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    diff --git a/logs/database.log b/logs/database.log index deb0266..1271c8a 100644 --- a/logs/database.log +++ b/logs/database.log @@ -652,4 +652,7 @@ sql='SELECT items.item_uuid as item_uuid, items.item_name as item_name, units.fullname AS fullname, units.id AS unit_id, items.links AS linksFROM main_items itemsLEFT JOIN main_item_info item_info ON item_info.id = items.item_info_idLEFT JOIN units ON item_info.uom = units.idWHERE items.search_string LIKE '%%' || %s || '%%' AND items.inactive IS false AND item_info.safety_stock > 0ORDER BY items.item_name LIMIT %s OFFSET %s;') 2025-08-21 17:32:19.598403 --- ERROR --- DatabaseError(message='syntax error at or near "%"LINE 1: ...CT COUNT(items.*) FROM main_items items LEFT JOIN %site_name... ^', payload=('', 25, 0), - sql='SELECT items.item_uuid as item_uuid, items.item_name as item_name, units.fullname AS fullname, units.id AS unit_id, items.links AS linksFROM main_items itemsLEFT JOIN main_item_info item_info ON item_info.id = items.item_info_idLEFT JOIN units ON item_info.uom = units.idWHERE items.search_string LIKE '%%' || %s || '%%' AND items.inactive IS false AND item_info.safety_stock > 0ORDER BY items.item_name LIMIT %s OFFSET %s;') \ No newline at end of file + sql='SELECT items.item_uuid as item_uuid, items.item_name as item_name, units.fullname AS fullname, units.id AS unit_id, items.links AS linksFROM main_items itemsLEFT JOIN main_item_info item_info ON item_info.id = items.item_info_idLEFT JOIN units ON item_info.uom = units.idWHERE items.search_string LIKE '%%' || %s || '%%' AND items.inactive IS false AND item_info.safety_stock > 0ORDER BY items.item_name LIMIT %s OFFSET %s;') +2025-08-21 18:44:41.870684 --- ERROR --- DatabaseError(message='column "name" does not existLINE 1: SELECT * FROM test_vendors ORDER BY name ASC LIMIT 10 OFFSET... ^', + payload=(10, 0), + sql='SELECT * FROM test_vendors ORDER BY name ASC LIMIT %s OFFSET %s;') \ No newline at end of file