From 6cee996fb3f376ee28d6de9b90ca53d3b793b55c Mon Sep 17 00:00:00 2001 From: c-d-p Date: Sun, 20 Apr 2025 12:12:35 +0200 Subject: [PATCH] [V0.2] WORKING Working calendar and AI with full frontend. --- backend/__pycache__/main.cpython-312.pyc | Bin 1766 -> 1915 bytes .../core/__pycache__/database.cpython-312.pyc | Bin 1633 -> 1633 bytes backend/main.py | 12 +- .../admin/__pycache__/api.cpython-312.pyc | Bin 1784 -> 2230 bytes backend/modules/admin/api.py | 28 +- .../calendar/__pycache__/api.cpython-312.pyc | Bin 3062 -> 2999 bytes .../__pycache__/models.cpython-312.pyc | Bin 1004 -> 1087 bytes .../__pycache__/schemas.cpython-312.pyc | Bin 1677 -> 2777 bytes .../__pycache__/service.cpython-312.pyc | Bin 3262 -> 3711 bytes backend/modules/calendar/api.py | 16 +- backend/modules/calendar/models.py | 4 +- backend/modules/calendar/schemas.py | 67 ++- backend/modules/calendar/service.py | 75 ++-- interfaces/nativeapp/package-lock.json | 8 +- interfaces/nativeapp/package.json | 3 +- interfaces/nativeapp/src/api/client.ts | 3 +- .../components/calendar/CalendarDayCell.tsx | 63 +++ .../components/calendar/CalendarHeader.tsx | 51 +++ .../calendar/CustomCalendarView.tsx | 170 +++++++ .../src/components/calendar/EventItem.tsx | 98 ++++ .../src/components/calendar/MonthView.tsx | 104 +++++ .../src/components/calendar/ThreeDayView.tsx | 115 +++++ .../src/components/calendar/ViewSwitcher.tsx | 41 ++ .../src/components/calendar/WeekView.tsx | 112 +++++ .../nativeapp/src/screens/CalendarScreen.tsx | 423 +----------------- .../nativeapp/src/screens/EventFormScreen.tsx | 77 +++- interfaces/nativeapp/src/types/calendar.ts | 14 +- 27 files changed, 996 insertions(+), 488 deletions(-) create mode 100644 interfaces/nativeapp/src/components/calendar/CalendarDayCell.tsx create mode 100644 interfaces/nativeapp/src/components/calendar/CalendarHeader.tsx create mode 100644 interfaces/nativeapp/src/components/calendar/CustomCalendarView.tsx create mode 100644 interfaces/nativeapp/src/components/calendar/EventItem.tsx create mode 100644 interfaces/nativeapp/src/components/calendar/MonthView.tsx create mode 100644 interfaces/nativeapp/src/components/calendar/ThreeDayView.tsx create mode 100644 interfaces/nativeapp/src/components/calendar/ViewSwitcher.tsx create mode 100644 interfaces/nativeapp/src/components/calendar/WeekView.tsx diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc index 58a6b67a8f2f39aee65ae2ff7b3dbceedf993ae5..8803303932803afa170c9f8afe037f57bc46c95b 100644 GIT binary patch delta 737 zcmZWl&ubGw6rS1aCfl@|q}jyH?pkZ1rABaT3fjh&9+c`u5F%cJP?pZireTwm*=?a- zR46DOq+$IN#7lY*{4W%wpobC=a`GJU$3ZX7Y_ihezVo79$o@nc!N875%-i^whJv{ z)6IQo`79wm@PNKs5z-iEgFCT@I3K)@-CQiN_8ySNKFo5r+6J~*RRtBmMO9A zDGZ*V{#5RWoJ^W0q8Sz!X9Hb&v86TJWX}c9+8+6~SOQ#+a#c@_SbNW}M@)({6At)y z+l~XYCwnY}eQU delta 533 zcmey(_l%eCG%qg~0}!ZvV$29(naC%>ST#{S)ryHBRUk_VB!LQ2I94-4M6y&-Wk5Wj z9<_=0WF0wESyS258B-ZkStNnNX-p6@g{y@nN(5-M$ZDwGC{ZAfJymoKH_X^5u~e}v z{mC|r^2uUZhEQclR4P{rPdZ}?Zweof1mZRPt63q^j0`}X#z;z`%oG8b*(ri83~-yJ zD%mxKCLds&xH*f7pOH~uaust7Bmd;<%uecJK%K=PZz>dt0?AtfIhkpx#RZ9Z@o9<4 zCHX~_MG`<^-pLLu28`m93t6NlFJbAl0EJc&$fFQVY#FJEIVBlI%0LlyATH(w5)BLw zctj?oT;P_zz#m)io%{KL~7D5y(wN z0+apNG&Mjb7NOeqi^C>2KczG$)vgE>W+1m0AD%p)O_uoqgW%+oY&r5bgtaaRY2Of& ex*;q!K^07V;AUVH{=&e<$n-&Iax}XPIIaMwkY@S- diff --git a/backend/core/__pycache__/database.cpython-312.pyc b/backend/core/__pycache__/database.cpython-312.pyc index 875c4e1f9580c1de7454e8453214fe74fb7d90e6..1502d8b636c8d90abdc9a57a3105b9a3cb585d90 100644 GIT binary patch delta 21 bcmaFJ^N@$Xu=q1RHm*}Z`XH(KTaL>#+=l47JckZ3}DZQ7S zA0T8*V5H|50cySRp_mFo2!`YRgE0b( z4MU7Hl*si8u%-}-45(wckf<9e4*uCGhk<9Wx8Ds0Oxuu#ur}g8s)ngFqz2qLkI+m& zbSPA$2~->(>{eni6Ff$VZra)u!x$+EWM%DSkQmZTp$`pfW*&Ni!b1sU+VusYdCZ1O zNHw=ou9I=(Pk~AB((~Mz1wI4#Ua9Hk$b4kI vq>byE{b9X_JwIJKcVX$;>eb~tnes+MZrWu?@^X_rv&Fc6Yrnm?% delta 676 zcmdlc_=A`4G%qg~0}z}PX2{sdJdsa=@!Lf8=OSrLDSRy~t3fhA5G7Q}uPLxujnRWq zI++n71}0g7Br_0yUI!GNKKVV9f+!1;G?baeKADe2N}`srhS7x~)`fwgmZ^p*g)!Tl zfuWeOhja1=CV5jV+G?02fd&B8GGuXL8d}4Y#SJu8TbdVBu7nRFHhBY!91o1cz>p<0 zIe|r%QEc)Y7I{XHW+^sC2B7JZAZ8RJLkd$0b0%XjgC@)ofJC2n!#rj{gvG)}(F>Q~QP zBn}iTk_O4J0u9SaEe3@g(3T=muxhaG_>`m~RUlUYh>Pa{i3Wx*Tnv0d{jr^~*Lf8$ z@+w~ERlCTmc9~bB!R>~i_$LMyR`m|n2iyV^+%I!W-w~0R?mN-8a;YKn*FY`NM2--oTD;dbEf8! zisf|+>n=)}US=`7&SHC!#r6t|{S6kjcF#u77VjH8q8G$9FK}yJV9_cP0lLsnQ+RR= z`+QBXyNiT@B4A;#uYPgZa7`NvF7WvKB|2Iv=C<1j!lZ3*A?^E%v`4 YZG2JC@G~<5lf*|RAPZ~?09hTHs{jB1 diff --git a/backend/modules/admin/api.py b/backend/modules/admin/api.py index 56f7761..048b1d9 100644 --- a/backend/modules/admin/api.py +++ b/backend/modules/admin/api.py @@ -14,18 +14,24 @@ def read_admin(): return {"message": "Admin route"} @router.get("/cleardb") -def clear_db(db: Annotated[Session, Depends(get_db)]): +def clear_db(db: Annotated[Session, Depends(get_db)], hard: bool): """ Clear the database. + 'hard' parameter determines if the database should be completely reset. """ - tables = Base.metadata.tables.keys() - for table in tables: - # delete all tables that isn't the users table - if table != "users": - table = Base.metadata.tables[table] - db.execute(table.delete()) - + if hard: + Base.metadata.drop_all(bind=db.get_bind()) + Base.metadata.create_all(bind=db.get_bind()) + return {"message": "Database reset (HARD)"} + else: + tables = Base.metadata.tables.keys() + for table_name in tables: + # delete all tables that isn't the users table + if table_name != "users": + table = Base.metadata.tables[table_name] + db.execute(table.delete()) + # delete all non-admin accounts - db.query(User).filter(User.role != UserRole.ADMIN).delete() - db.commit() - return {"message": "Database cleared"} \ No newline at end of file + db.query(User).filter(User.role != UserRole.ADMIN).delete() + db.commit() + return {"message": "Database cleared"} \ No newline at end of file diff --git a/backend/modules/calendar/__pycache__/api.cpython-312.pyc b/backend/modules/calendar/__pycache__/api.cpython-312.pyc index 8fa56dca5b776837d637935b68ebc665e0800b1f..146503687c8a58135743181648e3ced8bc0d04b2 100644 GIT binary patch literal 2999 zcmb7G&2JM&6rbJo`aAv(iS0OX0;CwEn3O<@3P~wVODak$D1z?Aa=eqobk}xg*N9Lf zIZ`D!BK1HxHNBLeAQh>XO8pbsgCn<6x>7_?4;6<>4rx`1Q{Rl&j-9vyR?5tq_vZcH z=e+pmt}Z8n@2C6x{0}yS{-B4(=Q4@y?|6jnAsNY>f>ci8R9@madgc{DwMZ653yP?U zlE`R_VpVODO|?sQb`}+f>Xe*}wkj^wEx8$OQ@T`-!5y?V6e1{dAUH%8{KU;FJi5b&uKUz8r>PD zmCBe%DNLb^UglOCFEz7}%F0G?uS7Gti_FY!lx`U-SjW5AHyz1_HN)PZAUW0o!L!*L zhDA@$X*X?2<_S%vhnW7R)I1;>V%Hghc2Pg+-$mlHg=f!CEuOWV(mdNthgYj?2SEe2xtGdQ3 zmT@u&(whOZp2;tnGPKey!`>h}S8xpU+QNEi1#bx@VJ9#@=r;lzrgXEAtggEP^ELyk zcRD!)DslMJXz5vnx(+U1TnT;O^RVafp{o1s^3=M^S3b|;d&R;CCB9=}P>I`Hm>}3~ z^M6-2wim{2zQKBX9R}S5b$+KbJMA_9qgZuKR4fzB^=H4(yPV;IHq(n*igtA8g6!$D zmllLW3(Y6tXl9r|m=@G@R=GHZnXk}-m9NNi+j#7!l=4M}6_ zjcC-lv_`r>Ogwa^)*>`BhLzO+30VqxO=r`^5TK*k8#RP% zPG|jWh=sJCnJ2V83~N2ngr@i*VtHg0cws+PcXWbGf{0E~?Jrns0>^QGqTvlRw1Hwz z(BK9-v4P?nD7k?SJVEjrlKGFN8VW*K2wfVxp$xXd)oll;|JFJhqfG6?yYrz6JX!8dut->0P7Cj zJ-6nH)ok==N6x_ALu-!cb9$SaI?L5~D)E`ND(-k?^nGgQgHEnnIsdxHPi^>_>#&=y z14Z%9F+6`AuWzxh;u)$OJ6GikB=6ICCo!EjLkFs|74_r^N-~a#s literal 3062 zcmb7G&2Jk;6rb4-dmVqb1@62DQNz zE&{c^RprodB0Z5RNO0tT;Nr?Xgb@)%AP_f)f{-}zW_F$0X%KX!oq6+S=J&qe?ziD^ z7lP-PPn7v_1EHV9qV)tl;{FWm!Me^;Hkb><9G8c3hVR^g$Z$gT?=oL6RgntF>g1ZmN^yq8}lWYEiQW z+0iC<=wIaaJc3odbZ1K2tJ`Ctn+gSCe=usNmlwgqlHVLC3VR}0WEwBGaL^^dJ zXhYt8F_|9xO+cp=9+-OL#Od;aLmBrUrxjWvb{cd48EV^watTO-C7Y&op32Kx*}R98r2>lgO0_z8*_n&k0^!MrcWjnaSnPLo_m=aMdxV7p@IRoi@io?KSAb}0Skk%f!gN+F8C=9l^ z0FD`9LqPB@kt1&S*cdQ&h$EsO)=)EoQvFM(Z@hNx`10A>^jaddl(><;HnyCs9s54G zbBQe<{9^Lf%090Dnu0A$xJbxVj^W$Y$v$ZJxrLBg0r z@Y*8nWMuUIrCm$p^SRIFmM`4Zo>&W|u76k$?yGD2q*>`P?w`#;g;okoSjdFCc*K6M zm@DSzsdaIp%q%ACWujhrpVuBHatqG<1d-ndTFMuw&EVUMcsO5XbOM@oZYBr)4gP`< zk4oO=7r>U8$CDjMvNhUF{6F#D)=sOtPS`?~oXx~f6bp$V{4WP7d%uvUvIn}UE=ynL zi0~-)G56D^Rm!#_yBJp?y}4E_zt`lBd>cvM$pHLq z9Cp<`%8tO6=-2koFg8_;@vms_I@-04hSt&3>u6*hW!BNy4~X1BU{SEx$kMZ6P zz_p>#wSnP{fPtS`12`4tdq%XW!wj&Y?(5{*^G&~48K^69{rI)v-^E!W=ooG)Vx!xW zR1b~RpL|J3d3ig<4IE#K#D#>oR2*hcai9d=D-tj6;tgINtVed$54=(vY~T|c#zT$> z`0(=*59WK|5FKW~3r3rMK^ej&;*3Ck6_5opCNv$oJXDYFZs4))l=R_o4|H5A32RSD Opaj&O4L}-~S?~{t#=)cj diff --git a/backend/modules/calendar/__pycache__/models.cpython-312.pyc b/backend/modules/calendar/__pycache__/models.cpython-312.pyc index aa9c860a9eee5d664695fe88713f12c1a260a091..31f3ae1d95e3f6ec046d47c62362bcd005b773e9 100644 GIT binary patch delta 409 zcmaFEzMq5lG%qg~0}zzXWXkw5kyny2Xrj8jeJ)!R8zVysLkd$4doD*52bj&A!MxQ1gj6Vw!u$&(mU z8AT`mU@TMQ0E*pWDM?H(zQvlHpOary#10Z+FD*_jiqA|b;+$N|TZlX-=wLks6Q-@_ey8kodsN$jJDd d!RtDM*Ifqny9{z)Soj#77$>-VWdPA&0{~~eSakpZ delta 322 zcmdnb@rIrEG%qg~0}z~cWXPC3kynz@Z=$-qH6ud`Lkd$4TP}MPJDAO!!;#Aw#mUIP z#Nf`5!qUQ!!kWstni-^%fgy^kl1-ESC5WlXIv9dBTr1DLkz$h-t z4`d0Xu%|Gt;aJTCQV7%~IC&3aDx=V3C8jcdcA&s5_R`|iqWH{|B96&BnLPQqfnr4< zf(?j^Gbal&dr!_|R^pL^iTi1CPoBf9T6v4FxG*O%CpjZEw^A>^D7T0MsNxoHa(+>& zUP@v~Vp1Z|Tuzv-B3Td%Y=}IN^^3zMH$SB`C)KV<8OUV>;$o2a2WCb_#^($!R~cOH YGN|8Wko&^I!|22~!R0Fhhz1(~09a*5z5oCK diff --git a/backend/modules/calendar/__pycache__/schemas.cpython-312.pyc b/backend/modules/calendar/__pycache__/schemas.cpython-312.pyc index 01698c91b1487c564ff47dabcae94bf77cd5f5ee..914fe741b643ade357b6b463570fa3da1346d2be 100644 GIT binary patch literal 2777 zcmcguOOF#r5bmCr$M0P>h&THHqO~^Qyd;7kQOE`X2^cAa9Og0_Jk74fjK}GjMKQ`@ zr6rOV#OoF z#bS=Y^UV+1^6d;Ezu{!_G1Ec61B1K7Cq4~GNE||OtOYdG94*uxUC#Bu2u;TfEyton zBWHGM@A z6Gyv3n9dBJo&wa@9Ftj0-veX7bi`N&Mn!NU+{(ntbU6M3J- zb>5KQF3dRbcp`LIE9@Yudw3$ktVcn_g^_qmv9PYPYY}JnsW3t~(0b}#`o%HcW`75W zyX0Hy)9dquaN36PWpZDu38S_W1ovSm^m-5rdLzb7GczxbSC^xZRqKAMS_iw;i>J?@ zu3q)(*CCcFcv=ZqT&<^{QH|@%EcD`It+v>MVQt`m?Jl_xrOA7E0M5Vq?!y^y)C$hoeH3C0E2{d&ayBEO+De z5NG?QSI=}l=q|2*_{5m*%^Y6+v@_p*|DJy5#N!;8IQ*;mM)#{9=bxGjJ#*rPrkj&} zomf)5^}>LuiV6c7q74NApMC+vHf`Dz=r%QUD~p@~PiqyOPs36^gJKrN$Tz=<7>eES zi}9Rpk)B~I|A-Iy-=gi$pxO3+1!|N7#AT)=ljUoYX4Ccd?)UbS0T?`kyT|OX6DVKLzk(-c|FOFptS=iFqtbmM(Bn zIg(k-pb5#mN?Q=Lh>WlM#zS+fMc6)c*H>DE>ZKXzrS|4ZC)dtaa$@Ju<$l59R@8*n zp*4JwS&12U8@{St<8up(0Nz<2D;>6jRc(n}maU*dYvZ#@>>a7lQIokz%w>y2ij z*de=yBny^AmH$;7;m#3%d;u{ON8uNL3<5}C=hx2miW6%Wdbv_3UazcO8Y%kbV9|eW z!1V7DHK+e1AXJoS5~IY+7Iv%cd(EU#=U9YQN3)W43mUTOagyJ%g~^&OnJFV1uJ|wQ zQ>w*&#E*j^yc`+wbDdK9J2~)-RG*RA4XZ>a?`1a#yf)@2%|6<@utDIxVWXmwD#~>F N(f*@<5qQgx{{eoxVb}lw literal 1677 zcmc&!%}*0S6rb6Tc1!tEL8QJ!omdMW-P{plkd%z(l#U>ousdCfA7tEv%lY){nXdjLttIo(l*9) zLcZa{=`x#6LxQtU7-6(T%ETcQeXT^xnxj#oktM?P9l{K*9YPIUj;>G>&`cX@C^QSG z#r0OMsnA?^ZndYF|AEDdG_jZ`xeUr2{M1PEXt3R4{tbE!b)W2WmyYAdr;L9C~Mb=0z_8eQ5g8sLTQM z2GJ-D)SgZu#088h5{A&bd}^>^l!VOqQ zSDNl74J7BRE2<0EML!uox8CeR6s`v^pRW{+F1nL}brF?aPf8K2ZAu<@@tS1Ptzr`T zU4dVW`s|SU=;Yo&O8*o9E5%HAVLb?CDj_Cxl@+6ip+WItBp2ke^4m-GzQOIse++6C zei1Nf3ry^w0cQbM3>g>F9p1))=Z69Kj5nQRD~DoLH4A^FkT;0_P|F8mK)g zzjLNfthw|{az|W+WRyO}Yx`5Dlzt}@U&-XLVbZCO#bW}0$Nimt;q$nSfkHFz8 diff --git a/backend/modules/calendar/__pycache__/service.cpython-312.pyc b/backend/modules/calendar/__pycache__/service.cpython-312.pyc index 407265d1ff95e21381db0a5efdbe939800e119ee..01ad4b5c98e24578989770b142b56edbd3692cb6 100644 GIT binary patch literal 3711 zcmcInU2Gdw7QW+|@l3{J$99^ZhMKeuC2_09w6u%(t0**(wzO1Dx>^}3<9coqC;ka{ z#uY&m;qDfZAOtO1DShZev_c}KeaOQ;l(#-2?Mr4|QDnkFE1FyBYO#N$M7LkTDE{F15lH(}v z%lYzrl4m@h6Y@e*V7!nM^Zult@nTNO%SoB>{#;vLNho`jGV^tf?Z3xQ6kq0^34SQiFad$SDe1wc0f@^G^pbfX}P%&PH@@9DAP zoGp!FUC$N^!16_s0wQZ^0~^^q20r*oI)@8dnhamY1p@>fg`$xfFO~{g3cs7dGX`Di zvpcrvy+SaUXc1nRp>}PbcFC4XIwq;CW-FMQO~FJu!@KuP37i_yaeV{2MP&M>fVMWG zZZ9uz8k*ychCNNlQVU}1dbn9`lDp%(%{eolb0%+c8h_LGIrj-SCzxo8?xUrdf~U_R z(D2aFq;=cn*?gdB!ezHu5Jpj4JZlR^dP0ZmmEclyB@M@YBn0Y&F29B1erM!|k*(y5 z8qTG(Qhvtf)0$@cGsS#9YuFOO;{@xIagk6ov$X`!jO-w_o-&Fl-6#@li|p2QdLg@M z`}vu~WHFBu8Eqz!0pp1?$4?$lTuNuA!SxCFTq%e3M8-uz0(^Hlo54dfvvy~Oc{=6I zrI=;X1D5H<=o8SD(Z;}`FW&##``1n_zVX=`cTbehtgAa8A|dEsQ+8C9K1=Ca?pl^t zmA;B{sK$eQBiOqZ?ygI~mWOK!5~UAcz4Ge((W__H1pkL8ubjMknlh)ZoSHA)7dqDh z;qvQs_!P25-AEJTtj)vDGXfR4O$N#k^|p^83Qf6Q_Y^2KV9sftkGDh3rq&d7NJ}%h z=|P0N^?)KDIdAghoGJW{ExN;&X7bapBcp+#rY}>!wlodVFaNfV>6<@FXGym!>dS`9&j4;*Sa)mH# zy1#8~d8?-)kFLD!ibl~b?rglx7T+mhGE3N_^^a$B1}4s%gtH++!ATy2&X$WrgK&6h z7M^o1x8?ORO{zp5hi>!5l5V=NV*(p3PfsU%VWx{J*Ptt-H8u3<$im29FILq4s=D7& z_uqN3df+ANz)SaDvksiDsIQlYAIeA$Uhh~7cP^^eRd}3Lwa-%fmijGq-@4kq9*$MR zd#v!Dr7UMsS9NwXsR{}~y%GgnQ#Y$aa4H)hf|Gr0b6sMho(8+vi7%W4d2wm6<8 zx!CfBQ2(mj&q6r!-j=9M-wyLxFGTA_ zWHe&7iA>J)-KKNUAje&M1$wCI(LN2RA zdIY=Mtpe)!gLEjs0NU``j3>bQ~d%mCFGX? literal 3262 zcmdT`O>7fK6rS1tIBUm&I0*<>A*oc1N-Ts>MX8{u4dti60kww+WSQMDS!W%G*)^@D zjg+*sHELBrt(3H?)E+n#amg{q9+7&%HC578kSbL@^k(Wxl{odyc-J@!X*s~5?=bJ} zdvD&%zW2?W`7s`kA)t@vg{eCVLcg((khDN(ErGCzOk`pkmGC6SOcrdRBuoli7HzR4 zO-fvrY`LUNDj>@Qn+j12lBr&iCbjU})QDjA5TT%%G0^vP^u6G#m^$cU5nUW~y%Ajk zbb7Tfll+5iGlPBgEO8vGJOiR;=3U}iB?5B%jl4}}%sidAOlDkAY@I2)xk9-zW9G>F z2AOqXjxU%Oe6`{bnzKwp41Sz3ND5-#t(!0`A{PVWWC|AYgkraEV7VReo7lu2w(w2- zIi3?eRFnfl7xTp1K}3weL+vsJVi=9vdmguMh#;Ot8Syk_mAi#9GG#0Jl4%*PFXm0t zSB!G0WVyadNr4h)DkD+0B%k2N2+L3oa+mjIE;|w!M}Qu_lAS7-NY*fCvqsq@*;7Z3 zAIVzlX(SqK&BU2mOC{JT@Z4GP!8+yq-s6#Dl%L|$ivgS5DMqd zNuEGQJ?TH5ia7ijcLf2_6Q{Rth$9u_8$H-ph_AEg7CxO(efd&_ z&}thwO2M*SLIZg4{N~uP@-;|uTN3lO4M6Llhz>wZnqtE+3=TseL(mBA2g%=hk+=~o zI5Z8G1amIKFpt*s#I<8rk9~Q*p^vTVyX*SybuG23rR!SyyP>+at0f9?Wq#s;g0%Qr zGPS7R(7!pqn%-Ye?_WvP(}x<#!!`LAHP&U@S5NO-cIxR@8_9z;89;OGz|{kv9=t0J z(yeS;S{wS2v(NU(yuy|ASSg_VR}BRVm+Q|nOw0rsFj z&>@(XQF(QEmFYH6_=07&%Pbcx>bR8Q`&L(Xpf7-eCSm9_g?2@UAFpWcLKWI59Rp{M zfpY9ruP1gk5@UC@F@ExS^){UL zPQRXg-lzT9)=cuG&1zQcgZxD(tZ9**stAR)H_>@$v*!RYbMlx%5mMktKL?;eJm2zgT2?Z86{v86|_MMq#72N&JRBvvBY3xh9PE-5*_>JT^Y zx|A}=Qq~CopN~2UM;^-oho3J_ld3OY&f68@Felz!!v2z~%(A;b(qd=^{Mg#g*DySL zC=QM+mT#0BgD+*GHY2s&Xv?+r6*G(_7Hk^aP zz)lhBv>@&O1?SCM5PA}vH#}m{zkwQ~YS6-Zo&sjRViKFc6~>V@3JHyLuT= start) - if end: - query = query.filter(CalendarEvent.end_time <= end) - return query.all() +def get_calendar_events(db: Session, user_id: int, start: datetime | None, end: datetime | None): + query = db.query(CalendarEvent).filter(CalendarEvent.user_id == user_id) + + # If start and end dates are provided, filter for events overlapping the range. + # An event overlaps if: event_start < query_end AND (event_end IS NULL OR event_end > query_start) + if start and end: + query = query.filter( + CalendarEvent.start < end, # Event starts before the query window ends + or_( + CalendarEvent.end == None, # Event has no end date (considered single point in time at start) + CalendarEvent.end > start # Event ends after the query window starts + ) + ) + # If only start is provided, filter events starting on or after start + elif start: + query = query.filter(CalendarEvent.start >= start) + # If only end is provided, filter events ending on or before end (or starting before end if no end date) + elif end: + query = query.filter( + or_( + CalendarEvent.end <= end, + (CalendarEvent.end == None and CalendarEvent.start < end) + ) + ) + + return query.order_by(CalendarEvent.start).all() # Order by start time def get_calendar_event_by_id(db: Session, user_id: int, event_id: int): event = db.query(CalendarEvent).filter( @@ -30,25 +55,23 @@ def get_calendar_event_by_id(db: Session, user_id: int, event_id: int): raise not_found_exception() return event -def update_calendar_event(db: Session, user_id: int, event_id: int, event_data): - event = db.query(CalendarEvent).filter( - CalendarEvent.id == event_id, - CalendarEvent.user_id == user_id - ).first() - if not event: - raise not_found_exception() - for key, value in event_data.dict().items(): - setattr(event, key, value) +def update_calendar_event(db: Session, user_id: int, event_id: int, event_data: CalendarEventUpdate): + event = get_calendar_event_by_id(db, user_id, event_id) # Reuse get_by_id for check + # Use model_dump with exclude_unset=True to only update provided fields + update_data = event_data.model_dump(exclude_unset=True) + + for key, value in update_data.items(): + # Ensure tags is handled correctly (set to None if empty list provided) + if key == 'tags' and isinstance(value, list) and not value: + setattr(event, key, None) + else: + setattr(event, key, value) + db.commit() db.refresh(event) return event def delete_calendar_event(db: Session, user_id: int, event_id: int): - event = db.query(CalendarEvent).filter( - CalendarEvent.id == event_id, - CalendarEvent.user_id == user_id - ).first() - if not event: - raise not_found_exception() + event = get_calendar_event_by_id(db, user_id, event_id) # Reuse get_by_id for check db.delete(event) db.commit() \ No newline at end of file diff --git a/interfaces/nativeapp/package-lock.json b/interfaces/nativeapp/package-lock.json index da08209..40cc055 100644 --- a/interfaces/nativeapp/package-lock.json +++ b/interfaces/nativeapp/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@expo/metro-runtime": "~4.0.1", "@react-native-async-storage/async-storage": "1.23.1", + "@react-native-community/datetimepicker": "8.2.0", "@react-navigation/bottom-tabs": "^7.3.10", "@react-navigation/native": "^7.1.6", "@react-navigation/native-stack": "^7.3.10", @@ -3037,10 +3038,9 @@ } }, "node_modules/@react-native-community/datetimepicker": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-8.3.0.tgz", - "integrity": "sha512-K/KgaJbLtjMpx4PaG4efrVIcSe6+DbLufeX1lwPB5YY8i3sq9dOh6WcAcMTLbaRTUpurebQTkl7puHPFm9GalA==", - "peer": true, + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-8.2.0.tgz", + "integrity": "sha512-qrUPhiBvKGuG9Y+vOqsc56RPFcHa1SU2qbAMT0hfGkoFIj3FodE0VuPVrEa8fgy7kcD5NQmkZIKgHOBLV0+hWg==", "dependencies": { "invariant": "^2.2.4" }, diff --git a/interfaces/nativeapp/package.json b/interfaces/nativeapp/package.json index 1246be1..44a4896 100644 --- a/interfaces/nativeapp/package.json +++ b/interfaces/nativeapp/package.json @@ -30,7 +30,8 @@ "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.4.0", "react-native-vector-icons": "^10.2.0", - "react-native-web": "~0.19.13" + "react-native-web": "~0.19.13", + "@react-native-community/datetimepicker": "8.2.0" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/interfaces/nativeapp/src/api/client.ts b/interfaces/nativeapp/src/api/client.ts index 08062e2..8a2f355 100644 --- a/interfaces/nativeapp/src/api/client.ts +++ b/interfaces/nativeapp/src/api/client.ts @@ -5,7 +5,8 @@ import * as SecureStore from 'expo-secure-store'; import AsyncStorage from '@react-native-async-storage/async-storage'; -const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:8000/api'; +const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://192.168.1.9:8000/api'; // Use your machine's IP +// const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:8000/api'; // Use your machine's IP const TOKEN_KEY = 'maia_access_token'; console.log("Using API Base URL:", API_BASE_URL); diff --git a/interfaces/nativeapp/src/components/calendar/CalendarDayCell.tsx b/interfaces/nativeapp/src/components/calendar/CalendarDayCell.tsx new file mode 100644 index 0000000..9d33bcf --- /dev/null +++ b/interfaces/nativeapp/src/components/calendar/CalendarDayCell.tsx @@ -0,0 +1,63 @@ +// src/components/calendar/CalendarDayCell.tsx +import React from 'react'; +import { View, StyleSheet, Dimensions, ScrollView } from 'react-native'; +import { Text, useTheme } from 'react-native-paper'; +import { format, isToday } from 'date-fns'; + +import { CalendarEvent } from '../../types/calendar'; +import EventItem from './EventItem'; + +interface CalendarDayCellProps { + date: Date; + events: CalendarEvent[]; + isCurrentMonth?: boolean; // Optional, mainly for month view styling + width?: number; // Optional fixed width +} + +const CalendarDayCell: React.FC = ({ date, events, isCurrentMonth = true, height, width }) => { + const theme = useTheme(); + const today = isToday(date); + + const styles = StyleSheet.create({ + cell: { + flex: width ? undefined : 1, // Use flex=1 if no width is provided + width: width, + height: height, + borderWidth: 0.5, + borderColor: theme.colors.outlineVariant, + padding: 2, + backgroundColor: isCurrentMonth ? theme.colors.surface : theme.colors.surfaceDisabled, // Dim non-month days + overflow: 'hidden', // Prevent events overflowing cell boundaries + }, + dateNumberContainer: { + alignItems: 'center', + marginBottom: 2, + }, + dateNumber: { + fontSize: 10, + fontWeight: today ? 'bold' : 'normal', + color: today ? theme.colors.primary : (isCurrentMonth ? theme.colors.onSurface : theme.colors.onSurfaceDisabled), + }, + eventsContainer: { + flex: 1, // Take remaining space in the cell + }, + }); + + return ( + + + {format(date, 'd')} + + {/* Use ScrollView for month view where events might exceed fixed height */} + + {events + .sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()) // Sort events + .map(event => ( + // Don't show time in month cell + ))} + + + ); +}; + +export default React.memo(CalendarDayCell); // Memoize for performance diff --git a/interfaces/nativeapp/src/components/calendar/CalendarHeader.tsx b/interfaces/nativeapp/src/components/calendar/CalendarHeader.tsx new file mode 100644 index 0000000..f679a62 --- /dev/null +++ b/interfaces/nativeapp/src/components/calendar/CalendarHeader.tsx @@ -0,0 +1,51 @@ +// src/components/calendar/CalendarHeader.tsx +import React from 'react'; +import { View, StyleSheet } from 'react-native'; +import { Text, IconButton, useTheme } from 'react-native-paper'; + +interface CalendarHeaderProps { + currentRangeText: string; + onPrev: () => void; + onNext: () => void; +} + +const CalendarHeader: React.FC = ({ currentRangeText, onPrev, onNext }) => { + const theme = useTheme(); + const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 8, + paddingHorizontal: 12, + borderBottomWidth: 1, + borderBottomColor: theme.colors.outlineVariant, + backgroundColor: theme.colors.surface, // Match background + }, + title: { + fontSize: 18, + fontWeight: 'bold', + color: theme.colors.onSurface, // Use theme text color + }, + }); + + return ( + + + {currentRangeText} + + + ); +}; + +export default CalendarHeader; diff --git a/interfaces/nativeapp/src/components/calendar/CustomCalendarView.tsx b/interfaces/nativeapp/src/components/calendar/CustomCalendarView.tsx new file mode 100644 index 0000000..08ee8ff --- /dev/null +++ b/interfaces/nativeapp/src/components/calendar/CustomCalendarView.tsx @@ -0,0 +1,170 @@ +// src/components/calendar/CustomCalendarView.tsx +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { View, StyleSheet, ActivityIndicator } from 'react-native'; +import { useTheme, Text } from 'react-native-paper'; +import { + startOfMonth, endOfMonth, startOfWeek, endOfWeek, addMonths, subMonths, + addWeeks, subWeeks, addDays, subDays, eachDayOfInterval, format, getMonth, getYear, isSameMonth, + parseISO, isValid, isSameDay, startOfDay, endOfDay +} from 'date-fns'; + +import CalendarHeader from './CalendarHeader'; +import ViewSwitcher from './ViewSwitcher'; +import MonthView from './MonthView'; +import WeekView from './WeekView'; +import ThreeDayView from './ThreeDayView'; +import { getCalendarEvents } from '../../api/calendar'; +import { CalendarEvent } from '../../types/calendar'; + +export type CalendarViewMode = 'month' | 'week' | '3day'; + +const CustomCalendarView = () => { + const theme = useTheme(); + const [viewMode, setViewMode] = useState('month'); + const [currentDate, setCurrentDate] = useState(startOfDay(new Date())); // Use start of day for consistency + const [events, setEvents] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // Calculate date range based on view mode and current date + const { startDate, endDate, displayRangeText } = useMemo(() => { + let start: Date, end: Date, text: string; + switch (viewMode) { + case 'week': + start = startOfWeek(currentDate, { weekStartsOn: 1 }); // Assuming week starts on Monday + end = endOfWeek(currentDate, { weekStartsOn: 1 }); + text = `${format(start, 'MMM d')} - ${format(end, 'MMM d, yyyy')}`; + break; + case '3day': + start = currentDate; + end = addDays(currentDate, 2); + text = `${format(start, 'MMM d')} - ${format(end, 'MMM d, yyyy')}`; + break; + case 'month': + default: + start = startOfMonth(currentDate); + end = endOfMonth(currentDate); + text = format(currentDate, 'MMMM yyyy'); + break; + } + // Ensure end date includes the full day for fetching + return { startDate: start, endDate: endOfDay(end), displayRangeText: text }; + }, [viewMode, currentDate]); + + // Fetch events when date range or view mode changes + const fetchEvents = useCallback(async () => { + setIsLoading(true); + setError(null); + console.log(`[CustomCalendar] Fetching events from ${startDate.toISOString()} to ${endDate.toISOString()}`); + try { + // Pass adjusted start/end dates for fetching + const fetchedEvents = await getCalendarEvents(startDate, endDate); + setEvents(fetchedEvents); + } catch (err) { + console.error("Error fetching calendar events:", err); + setError('Failed to load events.'); + setEvents([]); + } finally { + setIsLoading(false); + } + }, [startDate, endDate]); // Depend on calculated start/end dates + + useEffect(() => { + fetchEvents(); + }, [fetchEvents]); // Re-run fetchEvents when it changes (due to date changes) + + // Navigation handlers + const handlePrev = useCallback(() => { + switch (viewMode) { + case 'week': setCurrentDate(subWeeks(currentDate, 1)); break; + case '3day': setCurrentDate(subDays(currentDate, 3)); break; + case 'month': + default: setCurrentDate(subMonths(currentDate, 1)); break; + } + }, [viewMode, currentDate]); + + const handleNext = useCallback(() => { + switch (viewMode) { + case 'week': setCurrentDate(addWeeks(currentDate, 1)); break; + case '3day': setCurrentDate(addDays(currentDate, 3)); break; + case 'month': + default: setCurrentDate(addMonths(currentDate, 1)); break; + } + }, [viewMode, currentDate]); + + // Group events by date string for easier lookup in child components + const eventsByDate = useMemo(() => { + const grouped: { [key: string]: CalendarEvent[] } = {}; + events.forEach(event => { + if (typeof event.start !== 'string') return; // Skip invalid + const start = parseISO(event.start); + if (!isValid(start)) return; // Skip invalid + + const end = typeof event.end === 'string' && isValid(parseISO(event.end)) ? parseISO(event.end) : start; + const endForInterval = end < start ? start : end; // Ensure end >= start + + const intervalDays = eachDayOfInterval({ start: startOfDay(start), end: startOfDay(endForInterval) }); + + intervalDays.forEach(day => { + const dateKey = format(day, 'yyyy-MM-dd'); + if (!grouped[dateKey]) { + grouped[dateKey] = []; + } + // Avoid duplicates within the same day's list + if (!grouped[dateKey].some(e => e.id === event.id)) { + grouped[dateKey].push(event); + } + }); + }); + return grouped; + }, [events]); + + + const renderCalendarView = () => { + const viewProps = { startDate, endDate, eventsByDate }; + switch (viewMode) { + case 'week': return ; + case '3day': return ; + case 'month': + default: return ; + } + }; + + const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: theme.colors.background }, + contentArea: { + flex: 1, + justifyContent: 'center', + }, + loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' }, + errorText: { color: theme.colors.error, textAlign: 'center', padding: 10 }, + viewContainer: { flex: 1 }, + }); + + return ( + + + + + + {isLoading ? ( + + + + ) : error ? ( + {error} + ) : ( + + {renderCalendarView()} + + )} + + + ); +}; + +export default CustomCalendarView; diff --git a/interfaces/nativeapp/src/components/calendar/EventItem.tsx b/interfaces/nativeapp/src/components/calendar/EventItem.tsx new file mode 100644 index 0000000..1899181 --- /dev/null +++ b/interfaces/nativeapp/src/components/calendar/EventItem.tsx @@ -0,0 +1,98 @@ +// src/components/calendar/EventItem.tsx +import React from 'react'; +import { View, StyleSheet, TouchableOpacity } from 'react-native'; +import { Text, useTheme } from 'react-native-paper'; +import { useNavigation } from '@react-navigation/native'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { format, parseISO, isValid } from 'date-fns'; + +import { CalendarEvent } from '../../types/calendar'; +import { AppStackParamList } from '../../navigation/AppNavigator'; // Adjust path if needed + +interface EventItemProps { + event: CalendarEvent; + showTime?: boolean; // Optional prop to show time +} + +// Define navigation prop type for navigating to EventForm +type EventItemNavigationProp = StackNavigationProp; + +const EventItem: React.FC = ({ event, showTime = true }) => { + const theme = useTheme(); + const navigation = useNavigation(); + + if (!event || typeof event.start !== 'string') { + return null; // Don't render if event or start date is invalid + } + + const startDate = parseISO(event.start); + if (!isValid(startDate)) { + return null; // Don't render if start date is invalid + } + + const eventColor = event.color || theme.colors.primary; + + const styles = StyleSheet.create({ + container: { + backgroundColor: eventColor, + borderRadius: 4, + paddingVertical: 2, + paddingHorizontal: 4, + marginBottom: 2, // Space between event items + overflow: 'hidden', + }, + text: { + color: theme.colors.onPrimary, // Ensure text is readable on the background color + fontSize: 10, + fontWeight: '500', + }, + timeText: { + fontSize: 9, + fontWeight: 'normal', + }, + tagContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + marginTop: 1, + }, + tag: { + backgroundColor: 'rgba(255, 255, 255, 0.3)', // Semi-transparent white + borderRadius: 3, + paddingHorizontal: 3, + paddingVertical: 1, + marginRight: 2, + marginBottom: 1, + }, + tagText: { + color: theme.colors.onPrimary, + fontSize: 8, + }, + }); + + const handlePress = () => { + navigation.navigate('EventForm', { eventId: event.id }); + }; + + const timeString = showTime ? format(startDate, 'p') : ''; // Format time like 1:00 PM + + return ( + + + {showTime && {timeString} } + {event.title} + + {/* Optional: Display tags if they exist */} + {event.tags && event.tags.length > 0 && ( + + {event.tags.map((tag, index) => ( + + {tag} + + ))} + + )} + + ); +}; + +export default React.memo(EventItem); // Memoize for performance diff --git a/interfaces/nativeapp/src/components/calendar/MonthView.tsx b/interfaces/nativeapp/src/components/calendar/MonthView.tsx new file mode 100644 index 0000000..e72c161 --- /dev/null +++ b/interfaces/nativeapp/src/components/calendar/MonthView.tsx @@ -0,0 +1,104 @@ +// src/components/calendar/MonthView.tsx +import React, { useMemo } from 'react'; +import { View, StyleSheet, Dimensions } from 'react-native'; +import { Text, useTheme } from 'react-native-paper'; +import { + eachDayOfInterval, endOfMonth, startOfMonth, format, getDay, isSameMonth, + startOfWeek, endOfWeek, addDays +} from 'date-fns'; + +import CalendarDayCell from './CalendarDayCell'; +import { CalendarEvent } from '../../types/calendar'; + +interface MonthViewProps { + startDate: Date; // Should be the start of the month + endDate: Date; // Should be the end of the month + eventsByDate: { [key: string]: CalendarEvent[] }; +} + +const screenWidth = Dimensions.get('window').width; +const cellWidth = screenWidth / 7; // Approximate width for each day cell + +const MonthView: React.FC = ({ startDate, eventsByDate }) => { + const theme = useTheme(); + const currentMonth = startDate; // The month being displayed + + // Calculate the full range of days to display, including leading/trailing days from other months + const monthStart = startOfMonth(currentMonth); + const monthEnd = endOfMonth(currentMonth); + const displayStart = startOfWeek(monthStart, { weekStartsOn: 1 }); // Week starts Monday + const displayEnd = endOfWeek(monthEnd, { weekStartsOn: 1 }); + + const days = useMemo(() => eachDayOfInterval({ start: displayStart, end: displayEnd }), [ + displayStart, + displayEnd, + ]); + + // Generate week rows + const weeks: Date[][] = []; + let currentWeek: Date[] = []; + days.forEach((day, index) => { + currentWeek.push(day); + if ((index + 1) % 7 === 0) { + weeks.push(currentWeek); + currentWeek = []; + } + }); + + const weekDays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + + const styles = StyleSheet.create({ + container: { flex: 1 }, + weekRow: { + flexDirection: 'row', + flex: 1, + }, + dayHeaderRow: { + flexDirection: 'row', + paddingVertical: 5, + borderBottomWidth: 1, + borderBottomColor: theme.colors.outlineVariant, + backgroundColor: theme.colors.surfaceVariant, // Slightly different background for header + }, + dayHeaderCell: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + dayHeaderText: { + fontSize: 10, + fontWeight: 'bold', + color: theme.colors.onSurfaceVariant, + }, + }); + + return ( + + + {weekDays.map(day => ( + + {day} + + ))} + + {weeks.map((week, weekIndex) => ( + + {week.map(day => { + const dateKey = format(day, 'yyyy-MM-dd'); + const dayEvents = eventsByDate[dateKey] || []; + return ( + + ); + })} + + ))} + + ); +}; + +export default MonthView; diff --git a/interfaces/nativeapp/src/components/calendar/ThreeDayView.tsx b/interfaces/nativeapp/src/components/calendar/ThreeDayView.tsx new file mode 100644 index 0000000..05ab704 --- /dev/null +++ b/interfaces/nativeapp/src/components/calendar/ThreeDayView.tsx @@ -0,0 +1,115 @@ +// src/components/calendar/ThreeDayView.tsx +import React, { useMemo } from 'react'; +// Import Dimensions +import { View, StyleSheet, ScrollView, Dimensions } from 'react-native'; +import { Text, useTheme } from 'react-native-paper'; +import { eachDayOfInterval, format, isToday } from 'date-fns'; + +import { CalendarEvent } from '../../types/calendar'; +import EventItem from './EventItem'; + +interface ThreeDayViewProps { + startDate: Date; // Start of the 3-day period + endDate: Date; // End of the 3-day period + eventsByDate: { [key: string]: CalendarEvent[] }; +} + +// Get screen width +const screenWidth = Dimensions.get('window').width; +const dayColumnWidth = screenWidth / 3; // Divide by 3 for 3-day view + +const ThreeDayView: React.FC = ({ startDate, endDate, eventsByDate }) => { + const theme = useTheme(); + // Ensure endDate is included in the interval + const days = useMemo(() => eachDayOfInterval({ start: startDate, end: endDate }), [ + startDate, + endDate, + ]); + + // Ensure exactly 3 days are generated if interval logic is tricky + const displayDays = days.slice(0, 3); + + const styles = StyleSheet.create({ + container: { + flex: 1, + flexDirection: 'row', // Apply row direction directly to the container View + }, + // Remove scrollViewContent style as it's no longer needed + // scrollViewContent: { flexDirection: 'row' }, + dayColumn: { + width: dayColumnWidth, + borderRightWidth: 1, + borderRightColor: theme.colors.outlineVariant, + // Add flex: 1 to allow inner ScrollView to expand vertically + flex: 1, + }, + lastDayColumn: { + borderRightWidth: 0, + }, + dayHeader: { + paddingVertical: 8, + alignItems: 'center', + borderBottomWidth: 1, + borderBottomColor: theme.colors.outlineVariant, + backgroundColor: theme.colors.surfaceVariant, + }, + dayHeaderText: { + fontSize: 10, + fontWeight: 'bold', + color: theme.colors.onSurfaceVariant, + }, + dayNumberText: { + fontSize: 12, + fontWeight: 'bold', + marginTop: 2, + color: theme.colors.onSurfaceVariant, + }, + todayHeader: { + backgroundColor: theme.colors.primaryContainer, + }, + todayHeaderText: { + color: theme.colors.onPrimaryContainer, + }, + eventsContainer: { + // Remove flex: 1 here if dayColumn has flex: 1 + padding: 4, + }, + }); + + return ( + // Change ScrollView to View + + {displayDays.map((day, index) => { + const dateKey = format(day, 'yyyy-MM-dd'); + const dayEvents = eventsByDate[dateKey] || []; + const today = isToday(day); + const isLastColumn = index === displayDays.length - 1; + + return ( + + + + {format(day, 'EEE')} + + + {format(day, 'd')} + + + {/* Keep inner ScrollView for vertical scrolling within the column */} + + {dayEvents.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()) + .map(event => ( + + ))} + + + ); + })} + + ); +}; + +export default ThreeDayView; diff --git a/interfaces/nativeapp/src/components/calendar/ViewSwitcher.tsx b/interfaces/nativeapp/src/components/calendar/ViewSwitcher.tsx new file mode 100644 index 0000000..aacbbc1 --- /dev/null +++ b/interfaces/nativeapp/src/components/calendar/ViewSwitcher.tsx @@ -0,0 +1,41 @@ +// src/components/calendar/ViewSwitcher.tsx +import React from 'react'; +import { View, StyleSheet } from 'react-native'; +import { SegmentedButtons, useTheme } from 'react-native-paper'; +import { CalendarViewMode } from './CustomCalendarView'; // Import the type + +interface ViewSwitcherProps { + currentView: CalendarViewMode; + onViewChange: (view: CalendarViewMode) => void; +} + +const ViewSwitcher: React.FC = ({ currentView, onViewChange }) => { + const theme = useTheme(); + const styles = StyleSheet.create({ + container: { + paddingVertical: 8, + paddingHorizontal: 16, + backgroundColor: theme.colors.surface, // Match background + borderBottomWidth: 1, + borderBottomColor: theme.colors.outlineVariant, + }, + }); + + return ( + + onViewChange(value as CalendarViewMode)} // Cast value + buttons={[ + { value: 'month', label: 'Month' }, + { value: 'week', label: 'Week' }, + { value: '3day', label: '3-Day' }, + ]} + // Optional: Add density for smaller buttons + // density="medium" + /> + + ); +}; + +export default ViewSwitcher; diff --git a/interfaces/nativeapp/src/components/calendar/WeekView.tsx b/interfaces/nativeapp/src/components/calendar/WeekView.tsx new file mode 100644 index 0000000..545ff19 --- /dev/null +++ b/interfaces/nativeapp/src/components/calendar/WeekView.tsx @@ -0,0 +1,112 @@ +// src/components/calendar/WeekView.tsx +import React, { useMemo } from 'react'; +// Import Dimensions +import { View, StyleSheet, ScrollView, Dimensions } from 'react-native'; +import { Text, useTheme } from 'react-native-paper'; +import { eachDayOfInterval, format, isToday } from 'date-fns'; + +import CalendarDayCell from './CalendarDayCell'; +import { CalendarEvent } from '../../types/calendar'; +import EventItem from './EventItem'; // Import EventItem + +interface WeekViewProps { + startDate: Date; // Start of the week + endDate: Date; // End of the week + eventsByDate: { [key: string]: CalendarEvent[] }; +} + +// Get screen width +const screenWidth = Dimensions.get('window').width; +const dayColumnWidth = screenWidth / 7; // Divide by 7 for week view + +const WeekView: React.FC = ({ startDate, endDate, eventsByDate }) => { + const theme = useTheme(); + const days = useMemo(() => eachDayOfInterval({ start: startDate, end: endDate }), [ + startDate, + endDate, + ]); + + const styles = StyleSheet.create({ + container: { + flex: 1, + flexDirection: 'row', // Apply row direction directly to the container View + }, + // Remove scrollViewContent style + // scrollViewContent: { flexDirection: 'row' }, + dayColumn: { + width: dayColumnWidth, + borderRightWidth: 1, + borderRightColor: theme.colors.outlineVariant, + flex: 1, // Add flex: 1 to allow inner ScrollView to expand vertically + }, + lastDayColumn: { // Add style for the last column + borderRightWidth: 0, // Remove border + }, + dayHeader: { + paddingVertical: 8, + alignItems: 'center', + borderBottomWidth: 1, + borderBottomColor: theme.colors.outlineVariant, + backgroundColor: theme.colors.surfaceVariant, + }, + dayHeaderText: { + fontSize: 10, + fontWeight: 'bold', + color: theme.colors.onSurfaceVariant, + }, + dayNumberText: { + fontSize: 12, + fontWeight: 'bold', + marginTop: 2, + color: theme.colors.onSurfaceVariant, + }, + todayHeader: { + backgroundColor: theme.colors.primaryContainer, + }, + todayHeaderText: { + color: theme.colors.onPrimaryContainer, + }, + eventsContainer: { + // Remove flex: 1 here if dayColumn has flex: 1 + padding: 4, + }, + }); + + return ( + // Change ScrollView to View + + {days.map((day, index) => { // Add index to map + const dateKey = format(day, 'yyyy-MM-dd'); + const dayEvents = eventsByDate[dateKey] || []; + const today = isToday(day); + const isLastColumn = index === days.length - 1; // Check if it's the last column + + return ( + + + + {format(day, 'EEE')} + + + {format(day, 'd')} + + + {/* Keep inner ScrollView for vertical scrolling within the column */} + + {dayEvents.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()) + .map(event => ( + + ))} + + + ); + })} + + ); +}; + +export default WeekView; diff --git a/interfaces/nativeapp/src/screens/CalendarScreen.tsx b/interfaces/nativeapp/src/screens/CalendarScreen.tsx index 9215676..548aaf4 100644 --- a/interfaces/nativeapp/src/screens/CalendarScreen.tsx +++ b/interfaces/nativeapp/src/screens/CalendarScreen.tsx @@ -1,435 +1,42 @@ // src/screens/CalendarScreen.tsx -import React, { useState, useEffect, useMemo, useCallback } from 'react'; -import { View, StyleSheet, FlatList, TouchableOpacity } from 'react-native'; // Import TouchableOpacity -import { Calendar, DateData, LocaleConfig, CalendarProps, MarkingProps } from 'react-native-calendars'; -import { Text, useTheme, ActivityIndicator, List, Divider, FAB } from 'react-native-paper'; // Import FAB -import { useNavigation } from '@react-navigation/native'; // Import useNavigation -import { - format, - parseISO, - startOfMonth, - endOfMonth, // Need endOfMonth - getYear, - getMonth, - eachDayOfInterval, // Crucial for period marking - isSameDay, // Helper for comparisons - isValid, // Check if dates are valid -} from 'date-fns'; +import React from 'react'; +import { View, StyleSheet } from 'react-native'; +import { useTheme, FAB } from 'react-native-paper'; +import { useNavigation } from '@react-navigation/native'; +import { StackNavigationProp } from '@react-navigation/stack'; -import { getCalendarEvents } from '../api/calendar'; -import { CalendarEvent } from '../types/calendar'; // Use updated type -import { AppStackParamList } from '../navigation/AppNavigator'; // Import navigation param list type -import { StackNavigationProp } from '@react-navigation/stack'; // Import StackNavigationProp - -// Optional: Configure locale -// LocaleConfig.locales['en'] = { ... }; LocaleConfig.defaultLocale = 'en'; +import CustomCalendarView from '../components/calendar/CustomCalendarView'; // Import the new custom view +import { AppStackParamList } from '../navigation/AppNavigator'; // Define navigation prop type -type CalendarScreenNavigationProp = StackNavigationProp; // Adjust 'Calendar' if your route name is different - -const getTodayDateString = () => format(new Date(), 'yyyy-MM-dd'); +type CalendarScreenNavigationProp = StackNavigationProp; const CalendarScreen = () => { const theme = useTheme(); - const navigation = useNavigation(); // Use the hook with the correct type - const todayString = useMemo(getTodayDateString, []); + const navigation = useNavigation(); - const [selectedDate, setSelectedDate] = useState(todayString); - const [currentMonthData, setCurrentMonthData] = useState(null); - - // Store events fetched from API *directly* - // We process them for marking and display separately - const [rawEvents, setRawEvents] = useState([]); - // Store events keyed by date *for the list display* - const [eventsByDate, setEventsByDate] = useState<{ [key: string]: CalendarEvent[] }>({}); - - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - // --- Fetching Logic --- - const fetchEventsForMonth = useCallback(async (date: Date | DateData) => { - console.log("[CAM] fetchevents start"); - setIsLoading(true); - setError(null); - const targetYear = 'year' in date ? date.year : getYear(date); - const targetMonth = 'month' in date ? date.month : getMonth(date) + 1; - - if (isLoading && currentMonthData?.year === targetYear && currentMonthData?.month === targetMonth) { - return; - } - if ('dateString' in date) { - setCurrentMonthData(date); - } else { - // If called with Date, create approximate DateData - const dateObj = date instanceof Date ? date : new Date(date.timestamp); - setCurrentMonthData({ - year: targetYear, - month: targetMonth, - dateString: format(dateObj, 'yyyy-MM-dd'), - day: dateObj.getDate(), - timestamp: dateObj.getTime(), - }); - } - - try { - console.log(`Fetching events potentially overlapping ${targetYear}-${targetMonth}`); - const fetchedEvents = await getCalendarEvents(targetYear, targetMonth); - setRawEvents(fetchedEvents); // Store the raw events for period marking - - // Process events for the daily list view - const newEventsByDate: { [key: string]: CalendarEvent[] } = {}; - fetchedEvents.forEach(event => { - // --- Check for valid START date string --- - if (typeof event.start !== 'string') { - console.warn(`Event ${event.id} has invalid start date type:`, event.start); - return; // Skip this event - } - // --- End check --- - - const startDate = parseISO(event.start); - // --- Check if START date is valid after parsing --- - if (!isValid(startDate)) { - console.warn(`Invalid start date found in event ${event.id}:`, event.start); - return; // Skip invalid events - } - - // --- Handle potentially null END date --- - // If end is null or not a string, treat it as the same as start date - const endDate = typeof event.end === 'string' && isValid(parseISO(event.end)) ? parseISO(event.end) : startDate; - - // Ensure end date is not before start date (could happen with invalid data or timezone issues) - const endForInterval = endDate < startDate ? startDate : endDate; - - const intervalDates = eachDayOfInterval({ start: startDate, end: endForInterval }); - intervalDates.forEach(dayInInterval => { - const dateKey = format(dayInInterval, 'yyyy-MM-dd'); - if (!newEventsByDate[dateKey]) { - newEventsByDate[dateKey] = []; - } - // Avoid duplicates if an event is already added for this key - if (!newEventsByDate[dateKey].some(e => e.id === event.id)) { - newEventsByDate[dateKey].push(event); - } - }); - }); - setEventsByDate(newEventsByDate); // Update state for list view - - } catch (err) { - setError('Failed to load calendar events.'); - setRawEvents([]); // Clear events on error - setEventsByDate({}); - console.error(err); - } finally { - setIsLoading(false); - console.log("[CAM] isLoading:", isLoading); - } - }, [isLoading, currentMonthData]); // Include dependencies - - // --- Initial Fetch --- - useEffect(() => { - const performInitialLoad = async () => { - console.log("[CalendarScreen] Performing initial load."); - setIsLoading(true); - setError(null); - const initialDate = parseISO(todayString); - const targetYear = getYear(initialDate); - const targetMonth = getMonth(initialDate) + 1; - - setCurrentMonthData({ - year: targetYear, - month: targetMonth, - dateString: todayString, - day: initialDate.getDate(), - timestamp: initialDate.getTime(), - }); - - try { - const fetchedEvents = await getCalendarEvents(targetYear, targetMonth); - const newEventsByDate: {[key: string]: CalendarEvent[] } = {}; - - fetchedEvents.forEach(event => { - // --- Check for valid START date string --- - if (typeof event.start !== 'string') { - console.warn(`Event ${event.id} has invalid start date type during initial load:`, event.start); - return; // Skip this event - } - // --- End check --- - - const startDate = parseISO(event.start); - // --- Check if START date is valid after parsing --- - if (!isValid(startDate)) { - console.warn(`Invalid start date found in event ${event.id} during initial load:`, event.start); - return; // Skip invalid events - } - - // --- Handle potentially null END date --- - // If end is null or not a string, treat it as the same as start date - const endDate = typeof event.end === 'string' && isValid(parseISO(event.end)) ? parseISO(event.end) : startDate; - - // Ensure end date is not before start date - const endForInterval = endDate < startDate ? startDate : endDate; - - const intervalDates = eachDayOfInterval({ start: startDate, end: endForInterval }); - intervalDates.forEach(dayInInterval => { - const dateKey = format(dayInInterval, 'yyyy-MM-dd'); - if (!newEventsByDate[dateKey]) { - newEventsByDate[dateKey] = []; - } - // Avoid duplicates if an event is already added for this key - if (!newEventsByDate[dateKey].some(e => e.id === event.id)) { - newEventsByDate[dateKey].push(event); - } - }); - }); - - setRawEvents(fetchedEvents); - setEventsByDate(newEventsByDate); - } catch (err) { - setError('Failed to load initial calendar events.'); - setRawEvents([]); - setEventsByDate({}); - console.error(err); - } finally { - setIsLoading(false); - } - }; - performInitialLoad(); - }, [todayString]); - - // --- Callbacks for Calendar --- - const onDayPress = useCallback((day: DateData) => { - setSelectedDate(day.dateString); - }, []); - - const onMonthChange = useCallback((month: DateData) => { - if (!currentMonthData || month.year !== currentMonthData.year || month.month !== currentMonthData.month) { - console.log("[CAM] CAlling fetchevents"); - fetchEventsForMonth(month); - } else { - setCurrentMonthData(month); // Just update the current data if same month - } - }, [fetchEventsForMonth, currentMonthData]); - - // --- Calculate Marked Dates (Period Marking) --- - const markedDates = useMemo(() => { - const marks: { [key: string]: MarkingProps } = {}; // Use MarkingProps type - - rawEvents.forEach(event => { - // --- Check for valid START date string --- - if (typeof event.start !== 'string') { - console.warn(`Event ${event.id} has invalid start date type in markedDates:`, event.start); - return; // Skip this event - } - // --- End check --- - - const startDate = parseISO(event.start); - // --- Check if START date is valid after parsing --- - if (!isValid(startDate)) { - console.warn(`Invalid start date found for marking in event ${event.id}:`, event.start); - return; // Skip invalid events - } - - // --- Handle potentially null END date --- - // If end is null or not a string, treat it as the same as start date - const endDate = typeof event.end === 'string' && isValid(parseISO(event.end)) ? parseISO(event.end) : startDate; - const eventColor = event.color || theme.colors.primary; // Use event color or default - - // Ensure end date is not before start date - const endForInterval = endDate < startDate ? startDate : endDate; - - const intervalDates = eachDayOfInterval({ start: startDate, end: endForInterval }); - - intervalDates.forEach((dateInInterval, index) => { - const dateString = format(dateInInterval, 'yyyy-MM-dd'); - const isStartingDay = index === 0; - const isEndingDay = index === intervalDates.length - 1; - - const marking: MarkingProps = { - color: eventColor, - textColor: theme.colors.onPrimary || '#ffffff', // Text color within the period mark - }; - - if (isStartingDay) { - marking.startingDay = true; - } - if (isEndingDay) { - marking.endingDay = true; - } - // Handle single-day events (both start and end) - if (intervalDates.length === 1) { - marking.startingDay = true; - marking.endingDay = true; - } - - // Merge markings if multiple events overlap on the same day - marks[dateString] = { - ...(marks[dateString] || {}), // Keep existing marks - ...marking, - // Ensure start/end flags aren't overwritten by non-start/end marks - startingDay: marks[dateString]?.startingDay || marking.startingDay, - endingDay: marks[dateString]?.endingDay || marking.endingDay, - }; - }); - }); - - // Add selected day marking (merge with period marking) - if (selectedDate) { - marks[selectedDate] = { - ...(marks[selectedDate] || {}), - selected: true, - color: marks[selectedDate]?.color || theme.colors.primary, - textColor: theme.colors.onPrimary || '#ffffff', - // If selected, don't let it look like starting/ending unless it truly is - startingDay: marks[selectedDate]?.startingDay && marks[selectedDate]?.selected, - endingDay: marks[selectedDate]?.endingDay && marks[selectedDate]?.selected, - }; - } - marks[todayString] = { - ...(marks[todayString] || {}), - dotColor: theme.colors.secondary, - }; - - return marks; - }, [rawEvents, selectedDate, theme.colors, theme.dark, todayString]); // Include theme.dark if colors change - - // --- Render Event Item --- - const renderEventItem = ({ item }: { item: CalendarEvent }) => { - // --- Check for valid START date string --- - if (typeof item.start !== 'string') { - console.warn(`Event ${item.id} has invalid start date type for rendering:`, item.start); - return null; // Don't render item with invalid start date - } - const startDate = parseISO(item.start); - if (!isValid(startDate)) { - console.warn(`Invalid start date found for rendering event ${item.id}:`, item.start); - return null; // Don't render item with invalid start date - } - - // --- Handle potentially null END date --- - const hasValidEndDate = typeof item.end === 'string' && isValid(parseISO(item.end)); - const endDate = hasValidEndDate ? parseISO(item.end) : startDate; - - let description = item.description || ''; - const timePrefix = `Time: ${format(startDate, 'p')}`; - const dateRangePrefix = `${format(startDate, 'MMM d')} - ${format(endDate, 'MMM d')}`; - - // Show date range only if end date is valid and different from start date - if (hasValidEndDate && !isSameDay(startDate, endDate)) { - description = `${dateRangePrefix}${item.description ? `\n${item.description}` : ''}`; - } else { - // Otherwise, show start time - description = `${timePrefix}${item.description ? `\n${item.description}` : ''}`; - } - - return ( - navigation.navigate('EventForm', { eventId: item.id })}> - } - style={styles.eventItem} - titleStyle={{ color: theme.colors.text }} - descriptionStyle={{ color: theme.colors.textSecondary }} - descriptionNumberOfLines={3} - /> - - ); - } - - // --- Styles --- const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: theme.colors.background }, - calendar: { /* ... */ }, - loadingContainer: { height: 100, justifyContent: 'center', alignItems: 'center' }, - eventListContainer: { flex: 1, paddingHorizontal: 16, paddingTop: 10 }, - eventListHeader: { fontSize: 16, fontWeight: 'bold', color: theme.colors.text, marginBottom: 10, marginTop: 10 }, - eventItem: { backgroundColor: theme.colors.surface, marginBottom: 8, borderRadius: theme.roundness }, - noEventsText: { textAlign: 'center', marginTop: 20, color: theme.colors.textSecondary, fontSize: 16 }, - errorText: { textAlign: 'center', marginTop: 20, color: theme.colors.error, fontSize: 16 }, - fab: { // Style for the FAB + fab: { position: 'absolute', margin: 16, right: 0, bottom: 0, - backgroundColor: theme.colors.primary, // Use theme color + backgroundColor: theme.colors.primary, }, }); - // --- Calendar Theme --- - const calendarTheme: CalendarProps['theme'] = { // Use CalendarProps['theme'] for stricter typing - backgroundColor: theme.colors.background, - calendarBackground: theme.colors.surface, - textSectionTitleColor: theme.colors.primary, - selectedDayBackgroundColor: theme.colors.secondary, // Make selection distinct? - selectedDayTextColor: theme.colors.background, // Text on selection - todayTextColor: theme.colors.secondary, // Today's date number color - dayTextColor: theme.colors.text, - textDisabledColor: theme.colors.disabled, - dotColor: theme.colors.secondary, // Color for the explicit 'today' dot - selectedDotColor: theme.colors.primary, - arrowColor: theme.colors.primary, - monthTextColor: theme.colors.text, - indicatorColor: theme.colors.primary, - textDayFontWeight: '300', - textMonthFontWeight: 'bold', - textDayHeaderFontWeight: '500', - textDayFontSize: 16, - textMonthFontSize: 18, - textDayHeaderFontSize: 14, - // Period marking text color is handled by 'textColor' within the mark itself - 'stylesheet.calendar.header': { // Example of deeper theme customization if needed - week: { - marginTop: 5, - flexDirection: 'row', - justifyContent: 'space-around' - } - } - }; - - // Get events for the *selected* date from the processed map - const eventsForSelectedDate = eventsByDate[selectedDate] || []; - return ( - + {/* Replace the old Calendar and FlatList with the new CustomCalendarView */} + - {isLoading && } - {error && !isLoading && {error}} - - {!isLoading && !error && ( - - - Events for {selectedDate === todayString ? 'Today' : format(parseISO(selectedDate), 'MMMM d, yyyy')} - - {eventsForSelectedDate.length > 0 ? ( - item.id + item.start} // Key needs to be unique if event appears on multiple days in list potentially - ItemSeparatorComponent={() => } - /> - ) : ( - No events scheduled for this day. - )} - - )} - - {/* Add FAB for creating new events */} + {/* Keep the FAB for creating new events */} navigation.navigate('EventForm')} // Navigate without eventId for creation - color={theme.colors.onPrimary || '#ffffff'} // Ensure icon color contrasts with background + color={theme.colors.onPrimary || '#ffffff'} // Ensure icon color contrasts /> ); diff --git a/interfaces/nativeapp/src/screens/EventFormScreen.tsx b/interfaces/nativeapp/src/screens/EventFormScreen.tsx index b9c4d0a..2485d2c 100644 --- a/interfaces/nativeapp/src/screens/EventFormScreen.tsx +++ b/interfaces/nativeapp/src/screens/EventFormScreen.tsx @@ -2,7 +2,8 @@ import React, { useState, useEffect, useCallback } from 'react'; // Add Platform import import { View, StyleSheet, ScrollView, Alert, TouchableOpacity, Platform } from 'react-native'; -import { TextInput, Button, useTheme, Text, ActivityIndicator, HelperText } from 'react-native-paper'; +// Add Chip +import { TextInput, Button, useTheme, Text, ActivityIndicator, HelperText, Chip } from 'react-native-paper'; import { useNavigation, useRoute, RouteProp } from '@react-navigation/native'; // Conditionally import DateTimePickerModal only if not on web // Note: This dynamic import might not work as expected depending on the bundler setup. @@ -34,6 +35,8 @@ const EventFormScreen = () => { const [endDate, setEndDate] = useState(null); const [color, setColor] = useState(''); // Basic color input for now const [location, setLocation] = useState(''); // Add location state + const [tags, setTags] = useState([]); // Add tags state + const [currentTagInput, setCurrentTagInput] = useState(''); // State for tag input field // Add state for raw web date input const [webStartDateInput, setWebStartDateInput] = useState(''); @@ -60,6 +63,7 @@ const EventFormScreen = () => { setDescription(event.description || ''); setColor(event.color || ''); // Use optional color setLocation(event.location || ''); // Set location state + setTags(event.tags || []); // Load tags or default to empty array // Ensure dates are Date objects if (event.start && isValid(parseISO(event.start))) { const parsedDate = parseISO(event.start); @@ -112,6 +116,7 @@ const EventFormScreen = () => { setEndDate(null); setWebEndDateInput(''); } + setTags([]); // Ensure tags start empty for new event } else { // Default start date to now if creating without a selected date const now = new Date(); @@ -119,6 +124,7 @@ const EventFormScreen = () => { setWebStartDateInput(formatForWebInput(now)); // Init web input setEndDate(null); setWebEndDateInput(''); + setTags([]); // Ensure tags start empty for new event } }, [eventId, selectedDate]); @@ -275,6 +281,7 @@ const EventFormScreen = () => { end: endDate ? endDate.toISOString() : null, location: location.trim() || null, // Include location color: color.trim() || null, // Include color + tags: tags.length > 0 ? tags : null, // Include tags, send null if empty }; try { @@ -344,6 +351,18 @@ const EventFormScreen = () => { } }; + // --- Tag Handling Logic --- + const handleAddTag = () => { + const newTag = currentTagInput.trim(); + if (newTag && !tags.includes(newTag)) { + setTags([...tags, newTag]); + setCurrentTagInput(''); // Clear input after adding + } + }; + + const handleRemoveTag = (tagToRemove: string) => { + setTags(tags.filter(tag => tag !== tagToRemove)); + }; if (isLoading && !title) { // Show loading indicator only during initial fetch return ; @@ -435,6 +454,35 @@ const EventFormScreen = () => { placeholder={theme.colors.primary} // Show default color hint /> + {/* --- Tags Input --- */} + + + {/* Wrap Button text in */} + + + + {tags.map((tag, index) => ( + handleRemoveTag(tag)} // Use onPress for removal + style={styles.tagChip} + mode="flat" // Use flat mode for less emphasis + > + {tag} + + ))} + +