From 06356df2a395746e14707936e724787dd489e2fc Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Mon, 6 Oct 2025 17:53:25 +0200 Subject: [PATCH] Implements event layout coordinator Introduces a coordinator to manage event layout calculations, separating this logic from rendering. It calculates stack levels and allocates event columns for improved visual organization of calendar events. --- .workbench/image.png | Bin 0 -> 13607 bytes .workbench/scenarie3.html | 15 + src/data/mock-events.json | 429 +++++++++++++++++++++++++ src/managers/EventLayoutCoordinator.ts | 157 +++++---- 4 files changed, 545 insertions(+), 56 deletions(-) create mode 100644 .workbench/image.png create mode 100644 .workbench/scenarie3.html diff --git a/.workbench/image.png b/.workbench/image.png new file mode 100644 index 0000000000000000000000000000000000000000..17aa388c8a8dd58a365c560496d2c2764de19619 GIT binary patch literal 13607 zcmd73cTiK^7d8qpNRVR05)lxP-V7+c7m*H%C`|~UfPg5{OQ=#r1Vwsp(mM!93rO$1 z2-15GA(YU+gTB5m&fK|o=Kk@U?=Oa&v(H(3@3o(`*0Y}D_drFK^umn`czAfE@^bgo z@bK`B@bEzT5D@UoSnXUb9v&^8{5?r^2mH0zHFIj&xb%)A=4+cq;;TbPUqvBc`o#jwGiaBdVd-NuXEdv-bjA5 zJR$$l4Y*E8ggjBkg`>%3ui&O9hUz-_Z)r^u+Yov1U3RbyVWIDWfGNW71=3wKd!wyLAqi0Q;MvYJBnT-eje5Z z*IKRSDCp6k-K?-rEgjcb^i1-y>@7}~*Jsfw(+_4K*tb)<5OpPeDul>S%?GS}Zl*@t z+Oj4Gd+RD8%W%8$g^p-{c15P2UAFetkB(aOc4M@+a~jq=8vIA#u>tU862CUVlW{Bi zB~K^oR|Z)2HibHjkcgAz0`qHA?k^|-+^lHv?5Ymz_A$$`J$l!XRof_?ekh2Pj*AzR zd@#MTol-^{QL(c}Og-wKldIO~eUTf~&e*Q%H*ox6K5UuOq9g&3(9>(hURPJN$4$$^3lB z7f;xab#>DlxId`9QFvS91^KY*5Buq#sfw}|U-FH2wUkU5%R`Z{u1eUt=?8y%uR;aVP64wTa5r6<;Gz8VyMd)ahhJvw&%^*W7P zL3XQL6O3tOu1bVfSeCE>!jK&r1b-#Is5r0pwJ=OQE+Tu|`n~eF=!aDGu2G@I1-iyxGU#Nh^@W|b@I ztsEW_0pR;PGB~-e2QgaX#pvn?ox@r4*zyT6L9sw<+}!sC_GP~J?X%w6Qv)VL{StGV z2VJWN@XiUL2X6PVsnoUsPjX(WT6ZEnOubU1^FJ2zM6zGd#mMOGr045MQ; zE4MUT=5#h$3zs*nsA}xtW*oPA$(>q87t{}3vUwFa2Pd#X*T#OvjZ2v!Xfo*44FWVt zK?Bh1b+D@LeXa(;4=R?7n)MvMTJN?BViP|)T8FDU`94OF@6^s5jm0y?^OTj8p-1_S zmn~P;L;LnsHu{K%hRBYoF6!Jr-@o85OT$@}+@_%?RF!YzyvtRV@n!viY%V>el>QBH z<7NX7C`k!G?*eckN+8e;Cx&{+Bzwd83lRzE*=4R*4u`lm9DdJM_UPve=v&YQ_V@QC z@j$MZn*-rn1YPX4p-}f4`L1RUG$D;MS!g5JgAb~YoZ7|ZlUjrb@%jFRyMZ%) zV36&l8W;}r2cB&yVvnQ}1i>2wD0e*Y7x0F6h`KeEDg!j#|LoArw#8K#sDsWOzCi${ zrDb%7fB6zM<8aGELigXHjaxEXFB5S(hi{XbqBOFksUNRnThF`ZayaQpswZwm`pr=a zsfyhBIq#~Kv6ZfLK0L!8o9$zEj>WgJ;wCw;;^l%P6t>?6F=H@abKPw)pEYeU!Rm!; zsRi0acO+RT6_V6cgP$%w&Apu;5$$XGz~Om@zoNbkBK`IEm~E7zb`#USlxa!@eJC-} zIea3(wYeK3CV0abTFVD}bhqZGYTPLoiLZWkD$n_P;oM5|1?csK1FNmNK1IT*%oZl` z!Kj^0n=ra{q8$6!qUhR4g6->xuMRirC9X!;M(Ta8|GaKf1Jw?Ie-5?PSCNxHr}kRw z>W8qbntc^2p0BfX19s|z^O;}Qb2GgC1aCGibp)G*1fYAHc}oxTgK>`d_!J&NO)!Iy zFptoOgSVC2RH97pz$dlui*s`*Gz&ORAKDC&*oPn7VT;rmFfG$pjs1q=O7im;U0RB5 zq|m%$)q~GaMlJeUFe9kQ00rNo*Nvqf_87JuN_MXo&{hg_kKrZV5hE4+o}Qe*=OY_s;F2m zi(IX*ELtavQNrPLhHqR9tXK>0$E&b_@sAlGCZRJ)4lhXHku`FA9m-v@Wp?m|yMpe)@En z$9T{~g3hA_cgr+{iLf^nT~aH@E??UOw-Gbxt9iA%ZyU9@qoYNyiB1(Oyrjwg?NYg) zNvEWhjDGl&lBg7wpXST9on7~kiCIrI9UNU8VW1S?$jQasyR(ITPp+&1D5P}9pGze!?vx$98)#EkY0kJK z|LLmEI_7q!+(c_i76k_@Rh7l`U<`wLO4$?J)-KONbAzEVy;mP!tbVmI^B`S9RFlw_z*eZ zFKb4&xfaNySqv&p{jj0(yzP^{RLnekVB2v28Zza>>c?<&9=LpZ{EMLr6{d;mP zPEozIlk{ibJ@@RjL_0L%FMD{kR3F^{BN@&jGIEvl9R(;DKN?Si3k4&>NqAmM+K`9v7_|4nZl zj3k5h!6kg;!%K@J&gVhyHMq0u(T{tT3WnfR@DP*45D5u|1W}a0c(t{y*y4IBO{KH# zH{0FTr9E1R=U$P1!@<=N zEu)tip1b$*nI!Q-Z|ib#Cxxz({er`+fr(za^VsC7PHgA^WoaJ#-r%?6H^;X#No;3@ z3KOfr#^W&AYAv2rh|LMrM>v_7qw3sFYnrN!_{Q_yRJFzigh;auaf-QX6^aK2wjL7W zPjW06c-;DJV=7q$O=8AKkhr7g;v8{*d*FcjDsEFx#dN?ssxkO-Ba3aCggB%f1$V9b zcACCyv~{$xb=^Wx<-juvj&k09DLcV*Hqb)G9Z$3h>nmAwu?;(hP_PMlRF!O}@8WH+ zWd24LJeYA4CR>)c(}1a~3DqY@-xk>|$8r7}30+uwP z-+VZh82CV#4-+XN?DA&ITy$yj`F3FEk#|R3T&x%HjMpgh04*g$%Ic?jk8&w6h(TBn zd2taoHr}ly{zdNZtZZLCAqB-1972G~#~!A??09L|G!sO0Fs+Xc=YN%V8(x*1H{Tw6 z&|=rQr$U{$KfEZrB@nu1s_WGiFU}8_f>)z8wnV8_UWKO&#>W^xEKhx8{G8%0%7I@P zmEQ<4i}@sMU}_hu@v^tRS`Wb*p%(izoeckHT;(fa54%9P)ZI2qYj)AY!9$tyjV~Ig z`E_L%u}$w#eQzwx_=DxlPUc?eG0#JZ6Mr>C%of>tx}sogfD3b{ypP%AG;zLhGk%Y- z{b@^O-O$6HSRT`k?V#O3#FmlVQdFv9HBQp$Dv-l#jfgF(&Z7+5j;SVzi~Ok&{ZO-t z;IviRPL@bu@a%WrWudyIby(*S%+XP=e4gIr>Q4J+uFFZo=rnUf>H7;J0F9t39FLBs zwY|(CMnlxb&Kvr`goj<51!NHVbJ>49i;%xFi;_ac{_}zGgsyYy>$wuRkQcL&xev3i zbJjw?U`wR;>~ZI;VVVSK+1~uV)W;j0sHz{jRXw`p&)3l+)i=hZwC&T&Fh!PjL|+GC zQ}SpBH;Xw77iL`L{%BLr8=HuO&-u@ljl9BD$@p?P7lpqsOstL;-?md}guyS`p*e3y zbftl@CKFm5<#A?q8tNHKUP2XqbgQp_mS~$JCEI-Pz>Q3wwj16Y&UEaK`sJntD*prJ zKJIVoW|}@elc#dVRo+4d3FaopT`+hI2_gTTa<_$xY%!`7-T(lw24>UnhtSxpGNE;a zd{F%ZAb1El`2e)Bg1bBVEY5_WMbBZR^^yOgA`pGdko=E8U?lTd$VQ?m-qC<&fLHvN zr_WSGlG9EK|A`77CAeB7EO&oi1p8>y_BbVeA@=1-Lh^f>C81H*j^u%;$FVK7iC@o? zzjzDZDs{*r3~wMGo2Xo^0EmvIy0G?L7rx~Cq%WtvcqBob8{SUc2A!p7RjA>bOaQlv zA9Yl72n@=?#C3prA#@mFC;CC}OT(p~K|M>XDZCdJu-Bm$P9?DSn z{^5Y_T6^L`yr9xrPLIPw0nF#D9uGp^v<3Xar3sK zYr6~4gnjJ*Om(95kjR4@0$^(Eu#mq^b!^}j6&taiL@_^fiEb&6b?ZGksEvfSWXF&Z zX2*%&UAy7e-utOY)+9|{zr|lu$K_3Gh05S80hmc9ffvQ45|Dk##2L{Q)U{| z5DcofBF7f4GS*$#uBCw0$J*H;gKj;MU$>h#9Wn3u;TVld5gjn`+O$`7a*CN=XAk5G zkcGDlDSvNu`MJpq_5zRWRVLlL9Jv*EnPXv;&Q(!Wtz0lB)y2)DoN5L68#^j_Q zaup-*c%@AbTf3p5R8SPicWu=ms8_-2e80wbhv2Cw0v$=(a0zzgl!}z59hb?rHYrBv z$<1XIVZFF=>EZ9n&)Q%+-Vw8}h#$>MAnL8zTW-(De_9Q?8>RTf#)nhrr3q0m3>pCi z{7QyzSQCT~`EVZ(!S%rRpGvgwP1W#8|q( zS!21QB-~(2kHGuVR`AR?8{2Kd4#|Y`pd5^hk*Azkdzc$x?vBij566pyt<5HnrNt&! ztUx%!-T*5Bgcq~f5$?cQjuOHXeVQNzGj*?CLyWs^z7q7<*{|JuVA@|lhK|k`FO7mfOlLz6UR@7GM@5aao{uLF_3mOC|JxM^=cjqL)+@bkfd`FtwO^UX#rJ zfGe8CFi#waiHr5`K_;y0^y}FH!@Qjx@0ll6XK5h9OardOGE5Ff}BaH1>HU9m)8M`0})Uduaump}D#*M4zHeAY~T z+`S@|U;rLjCl*~99{w?0`7-{BrU{MKoazYd$b@BBv=9s;rR#P=L^Q#MC-QOva#Oi1 z-fjU!2UOo3uAmz%deFR@Ubc`+YFg;^klrI3Zj5%6DL;pf_RnYHczoPseR(H{t8HfO zvw2p_5zN$Zg?FYcYu?IlyuD15l9c1_cg>(5&I`QYDA|PAE_SmYf(F3y0O$ zJsP3i*Wh%-#h>um_#zZza$XBb(=fg3!1GweC8;1p%3;SlggxKxlfe+pS_MqtM!IfV z1l3`?;JMF37tZ{2xbBPdloSKsP_Ef-SlO^u#=<57xHGY2Y9jMv{L)4Xtm(kH8i&~{qlIuFsL&r>4H_zvWon@5 z;qLq>+>nk*F)QdN%!v*q=c~QV$Fw7c1y^Dk)^mz9!b*4ISM7kqqxbH#3Szr882~r5 zMh~O7h$E}tO{w?4_@QeKthW`(p9(W)O)XsjOnx>hvpwVh=vM8sKV=r{UbyNjA@#@f z(-_bAevSaFtq9Cw_6z^6d2z{}8s zG+^2X690Jb`-8P0d;U$IxHhqc{tq`K^)mrz(*l}0ziZtLv*=p7NrJNwIOly#L=JlN zSDBZZX>q~V9d;HKLty*NauB7^v;G#9S;9&M(gP~&|56LR#dumeuE+mQ7PV(6D?Y9y zO8)SKLn5OPcXc?Fy%hBW^-=A(9Frvw#FyfA;g&qsL_Fi+Pk&iEw?o> zS4=-6jtp8O{B3R%tf3D4uK_hM!MC{_Os7RZvuKY0#vXQIUgo-wNPLw8t+c;~L>mzo zO8-CdBFvscody*1krt>mfrjD-xR1Nc#d98Ef2DhzXw7sna74AlHGtlQg0CY;@cnO| z2XX$DCu)xuJaB<q8e#FTKb~gEB?qKtg!n{fPXS{Ad__XPH|7d#GpY~Tli@sGC>k(-%*@)C|~e+oZp-N0=xOj ztNKV1Cw?4FF6%eDKR%88auMPtrLO>vn)B~r^Ml`?X?y~`4p2YW4QcOkJO!FaA1*cT zPa7Q0v@?;|dOqN$Z-iYvXJT)`s6U!tRYk2hJ;iDPJ^EEZbe8blxo=q{4FEt5tjOAi z`wEyz5f7nHcuGE;?wRsnU&wiUnzK`5rExxI=u`85J5_YnsiLye0IEog99R~C<0(g( zbT9~-PjsDM#Pv`hte(4zeRX}SEt4tkeVnKPqn+>R;rc6|Oz)tkcwObY5K zN)yF@2XVP;VjTbyG+JOf#2W(wHYQCv1z$g#VaWg0W-q>k8@q^+vDmmwK(Y-&IM~(r z2$d`A?Ny{r%~RrPi{^ddBEtI0PipcDxG_8&1tUXEFkBMAY$oj-((vISAL^8lIHw!G zc0o>!aX0+F1VZ$8<6hW}^9o<)BJ8X<*Mv(t|J`v&A@xdrJ1mcEsNyw2Y{1c?T$RX= z2~E`gJQ4~JJpgs{vjVX&k+g5P+u0gwZL-k+9_ZgI?w06PejO4K*`cQ2qkJv$Y+c5 z@2MC4XO5~6hqy`=Ts_P=i|wcjHK0{i_RaEs#pL0pp5D!M+ikPcCN1J!Ho!2=MEooY zASlKhri2cdQ|opD-T%$??wnij{^S7fJ$^XnSmm38`DvMF0gCo&&tzON1~_T20(Qc*|!xy(`|!VC>w! zu-V0Ew^po7OO))2;xcl1P_nR-2K|)8b24@}o>7s_*#qwL;#w$mXxM7{D2&^yN(@oo z*5M)iEm`PiXWsT|W2vzH#0Ag2&aj`p)lQhX^<_rRSUJMf_E6Ov{AO62g5p^0n-OV) z^og6RuMcrZ=$AARf<&KTUVB^pA#oL7wnmp0%@%??Du-`N8(?oK9y^L?7w2JIF2(+^ zFKG`kaU)-Uy1mh^o$Hmk{21y(t8}I z^j?Y9-Ay_BeUwAF%goMl0Par5VQtj}OgZCPj;YGGQICtL>&zCw1n%nQmA?;BKpJFyo1p|#g8 zMAXX8U^B#lZJ5k}n&X95QK=h zf#B!2yZ}EZ%oQivea))X&WXhN;xaQ|qUSHXEBkCHwB}FqC7hw7qHM@$jx$YMoj+eA zUav3ghDgeCw&Sy)3CUjDXx9Dr0F5@O};HJ#ItV< ze!k_D8e}dtjoFe~Kz@OV^@cNaaiB+b5$&9=YL;2)`9a@M->MJ9gDD2}CbWEX^!K7J zim4CTeP)y0MSRHM<99zaQ0WxH-6CxLkYe;K>#irfy#um*+(`^`dwx{06I7mSfUxNv z+Z-;10m_SV#qjnV#DYe!u)xLLCtVri$L&&9ZYG$@44&Ahh;*%%J$_;K<@0@kW$eh~ z<&nGAe)VMAUu-&pPf5zHPs=vUW!2cRZ!zyL-Ud!o@ulnEVaArZ4z*LyVPD&=r)ub=*fm>OyI^J~`P(!mdP`rgQ2wZ> zE<;#~7TYLWc+DIYhnCtt+)utO+muWgN$UKL5oZX|yjp>rw`ax@5T;84J;+H%8B!sU zq8t+B)KAQuqYQ7r3eueA(Hj=jMlQwkCYY1o@m$*!z&fIDG<`icU!xqoQQy`Cd$c6T z>>6WSC;@=9!T%>nbD<*0e=J?~hi}LLjjo0*a%Tm`8UK+&BL&(1WJ8R%EMc41``;G{ zQ0a-7p4CcoJ0^Xy7<6-XT!^ZVGZAtwDtAd~(}j5{F;jj&>)wvfPd z*@kK2k7Qv)SASn5Mn06DNgj3_T2-o{5=M3`7GOr$<;l6EQ_ z7(!s@L{JPBD&NVm)KA~Iw8p7)*N6GRqa4&PM2%#73w6WxBcIu6uiLupHmLBss6{#! z`e^`VLQ$C4%`#(mxQ$TCFt7i_OP)r7lsLPUD){j;I92zmaY3vjgZJkLgj52yOm}eK zSAgn!BP=sn)IGBaWc6VDV0zXO*X6dtBCY2*m>ph&hlr4chLa8@n-JU&#$Srsq4Ed~ zcxa`qBWS`uxJe7iF70R0H^6c^aV4o~+CA03IYcF6XrZGbz&31ST7HPRi4(CM!=e54 zaEp4li*BK8$%hckD{BbJrc63bdt!WgWqb#i)0^k+Zh0f@)u7;A+Ma&XJH|bX3}M4d z+GWd?DMjVhf@2!-@cmod;{_s*d1}`vCCY;M7r2Il+CAYaPE)`8VL^9Rmoeon;PVGk zMp2#j2i`C{=8h)b(`U3)>u9$sc4Ppp5j#sU2rJ387C5_BMHSgQOFMTsSevI&Xf0Z7 zOEAEXzjGDkL^XF-zS%`)<=P3haaN7E)Sj@$Tdq zBHkB>B2l_5=E}Gs)4$e~#vFEOYuGn>V6b9uo_r4wm|c1NJnDz_80XW7?S?uWbL=Iq zrpKDMvLq$s^feKh!=co&*U>P}A#~UW_#H=cio{Xavk&l)E1S0TV9hXpNZWpL<__;y{jBkDyYO~bV?b$SNNo@1WvthHp5(u|4QLDe! z?2tKS!F!r`pVM}%`T}4R%9j~1jmtZMzfY#jt+KRgW4s8y` z(KpT=<85!@jv?^O`jvSW^NOLnFLMD%Ldu1buq;09m?xDqE>Jsx8e!wb_>C|~E+6Qe zOQQIv8xuUH$6eK3M!I#}?>MOp|2an+i2hMxrIcA@Z@{m+?r(Xo z_a3NF#RL-^1?{{TsT0zAkalr-wQ*nF{P5Mu=)KBOm76B8o#FQ*vDme^ohNs9ZXo)@ zIPVUAzMtOt4z*}heMOeF21)+3i4ZV1$5@Uff4FElm6d1@ew4(v)(ut617?X!xj2K znEFVxorH`$%yVl?s}zwyaUQzL^uvzn>k47rkT*>TVW*IY;C`VZ`njY55GIi z&53xzpeb5{b3z*S!0n)7Gu6_~kXCA%%wuAkqB0(4#q4G#h-dVfzr+e2TdNW5` zAPl~hy)aiSy6{GFP?KKK>ISO2Q9f0}q?&Wee^|V6xZSk*GX`mJ(=f`_zceFcc`Y_8 z8nf%OI~pX{iM!=I++a$%KKsJK@OGcTfz{w^$ZU&G&}l=f4*TBtWJ9%tn^Y&N8Fqxd z)Ux&bFkl=r*ZUk@-lfW?D10J0<3Ee(#H?mGyCy1!T*=boIjpF>OdITMFuH-m%xID~ ziR3j~Fa|#haM>p!*&^78zWKS-{B;EFHz4!?(M4wlOD2jx zLyR85n;`~k{3^}N%#q`X%z*e!mr@_;F=<8*@DVZ=0bRmdoELGwil0t>fY0d0_lyB~L73YZuuK%a5owkc=BqIhSeZ>G9!bxMG z<^my+!~+%it{E5jyaeu>MOv zGcOP)NBaPFcIe~qo?$pRN*c()<21Y^0Ve7xNF-10@JrY~Hv{yu6uhU}LGPiitT&)F zj0A7Utbg-a7pg!$vmoD&~$HrEQl)`uyXjp3cexphYt8n z+BYLjlKWMFo1y_K7|`l-`1!UYxZD6u(-r9N=CVIVW=tqNklgPwAV95w%`*HET4rMS zojoLi&(c8|OgiyDXdjUNwG#o=!^Geyr9baIr6G8S_v_DlnEkbhW``Ymv;diVMgx`E zGRVvaQI|eD^zqE=9gqjpU$mrhiCBST!i~R%-f$~|L3kqnVmb=9{Z3W1XIbU}Ty&L0 zLd$QVD?oIxdEkLxf*MIb(ufB#zG=t4zYy2o`+VgxB{-Ar6E$LyaUa&jo;iO#9|pkz zr2CILPCaQnkP+ZK6x4`p)fi~H&)HXIHcqS(fRz8@AfsNo7`~O@{mYAZkB#Ag@cHbz z4gB-gv(Ep&Tk_xdA(96P1^8J${5XyJK0iQn#{J*1;e95u1HwlDd+7YF!5n~?4|ph+ zkE$=dk! z;f-|E2H|tE#UFI%F!N@Q6dow}*2WRPA4hHe3uI6r zj{|VG1nsQ{mSP|p2;e>xpg{P|P2htZiG+w>-vg!cPDElZ5Ci@M5tt#i1a5({0PChO zxp56lI|-amGO}ZU#a95zIdXwY;AsMY0Q~J+aR9}EepnKi>@}b_2NX)M6PoiE8HHZG U)wTpafWniPQMs2ZrSJKF07&9w*#H0l literal 0 HcmV?d00001 diff --git a/.workbench/scenarie3.html b/.workbench/scenarie3.html new file mode 100644 index 0000000..e18f154 --- /dev/null +++ b/.workbench/scenarie3.html @@ -0,0 +1,15 @@ +
+ 10:00 - 13:00 + Scenario 3: Event B +
+ 12:30 - 13:30 + Scenario 3: Event D +
+ 09:00 - 15:00 + Scenario 3: Event A + + 11:00 - 12:00 + Scenario 3: Event C + + 16:00 - 22:00 +
\ No newline at end of file diff --git a/src/data/mock-events.json b/src/data/mock-events.json index 7a2f5d6..4eb18d7 100644 --- a/src/data/mock-events.json +++ b/src/data/mock-events.json @@ -2246,5 +2246,434 @@ "duration": 240, "color": "#2196f3" } + }, + { + "id": "S1A", + "title": "Scenario 1: Event A", + "start": "2025-10-06T05:00:00Z", + "end": "2025-10-06T10:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 300, + "color": "#ff6b6b" + } + }, + { + "id": "S1B", + "title": "Scenario 1: Event B", + "start": "2025-10-06T06:00:00Z", + "end": "2025-10-06T08:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#4ecdc4" + } + }, + { + "id": "S1C", + "title": "Scenario 1: Event C", + "start": "2025-10-06T08:30:00Z", + "end": "2025-10-06T09:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ffe66d" + } + }, + { + "id": "S2A", + "title": "Scenario 2: Event A", + "start": "2025-10-06T11:00:00Z", + "end": "2025-10-06T17:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 360, + "color": "#ff6b6b" + } + }, + { + "id": "S2B", + "title": "Scenario 2: Event B", + "start": "2025-10-06T12:00:00Z", + "end": "2025-10-06T13:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#4ecdc4" + } + }, + { + "id": "S2C", + "title": "Scenario 2: Event C", + "start": "2025-10-06T13:30:00Z", + "end": "2025-10-06T14:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#ffe66d" + } + }, + { + "id": "S2D", + "title": "Scenario 2: Event D", + "start": "2025-10-06T15:00:00Z", + "end": "2025-10-06T16:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#a8e6cf" + } + }, + { + "id": "S3A", + "title": "Scenario 3: Event A", + "start": "2025-10-07T07:00:00Z", + "end": "2025-10-07T13:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 360, + "color": "#ff6b6b" + } + }, + { + "id": "S3B", + "title": "Scenario 3: Event B", + "start": "2025-10-07T08:00:00Z", + "end": "2025-10-07T11:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 180, + "color": "#4ecdc4" + } + }, + { + "id": "S3C", + "title": "Scenario 3: Event C", + "start": "2025-10-07T09:00:00Z", + "end": "2025-10-07T10:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#ffe66d" + } + }, + { + "id": "S3D", + "title": "Scenario 3: Event D", + "start": "2025-10-07T10:30:00Z", + "end": "2025-10-07T11:30:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#a8e6cf" + } + }, + { + "id": "S4A", + "title": "Scenario 4: Event A", + "start": "2025-10-07T14:00:00Z", + "end": "2025-10-07T20:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 360, + "color": "#ff6b6b" + } + }, + { + "id": "S4B", + "title": "Scenario 4: Event B", + "start": "2025-10-07T15:00:00Z", + "end": "2025-10-07T19:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 240, + "color": "#4ecdc4" + } + }, + { + "id": "S4C", + "title": "Scenario 4: Event C", + "start": "2025-10-07T16:00:00Z", + "end": "2025-10-07T18:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#ffe66d" + } + }, + { + "id": "S5A", + "title": "Scenario 5: Event A", + "start": "2025-10-08T05:00:00Z", + "end": "2025-10-08T08:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 180, + "color": "#ff6b6b" + } + }, + { + "id": "S5B", + "title": "Scenario 5: Event B", + "start": "2025-10-08T06:00:00Z", + "end": "2025-10-08T07:30:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#4ecdc4" + } + }, + { + "id": "S5C", + "title": "Scenario 5: Event C", + "start": "2025-10-08T06:00:00Z", + "end": "2025-10-08T07:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#ffe66d" + } + }, + { + "id": "S6A", + "title": "Scenario 6: Event A", + "start": "2025-10-08T09:00:00Z", + "end": "2025-10-08T12:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 180, + "color": "#ff6b6b" + } + }, + { + "id": "S6B", + "title": "Scenario 6: Event B", + "start": "2025-10-08T10:00:00Z", + "end": "2025-10-08T11:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#4ecdc4" + } + }, + { + "id": "S6C", + "title": "Scenario 6: Event C", + "start": "2025-10-08T10:00:00Z", + "end": "2025-10-08T11:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#ffe66d" + } + }, + { + "id": "S6D", + "title": "Scenario 6: Event D", + "start": "2025-10-08T10:30:00Z", + "end": "2025-10-08T10:45:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 15, + "color": "#a8e6cf" + } + }, + { + "id": "S7A", + "title": "Scenario 7: Event A", + "start": "2025-10-09T05:00:00Z", + "end": "2025-10-09T07:30:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 150, + "color": "#009688" + } + }, + { + "id": "S7B", + "title": "Scenario 7: Event B", + "start": "2025-10-09T05:00:00Z", + "end": "2025-10-09T07:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#ff5722" + } + }, + { + "id": "S8A", + "title": "Scenario 8: Event A", + "start": "2025-10-09T08:00:00Z", + "end": "2025-10-09T09:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#ff6b6b" + } + }, + { + "id": "S8B", + "title": "Scenario 8: Event B", + "start": "2025-10-09T08:15:00Z", + "end": "2025-10-09T09:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 75, + "color": "#4ecdc4" + } + }, + { + "id": "S9A", + "title": "Scenario 9: Event A", + "start": "2025-10-09T10:00:00Z", + "end": "2025-10-09T11:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#ff6b6b" + } + }, + { + "id": "S9B", + "title": "Scenario 9: Event B", + "start": "2025-10-09T10:30:00Z", + "end": "2025-10-09T11:30:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#4ecdc4" + } + }, + { + "id": "S9C", + "title": "Scenario 9: Event C", + "start": "2025-10-09T11:15:00Z", + "end": "2025-10-09T13:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 105, + "color": "#ffe66d" + } + }, + { + "id": "S10A", + "title": "Scenario 10: Event A", + "start": "2025-10-10T10:00:00Z", + "end": "2025-10-10T13:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 180, + "color": "#ff6b6b" + } + }, + { + "id": "S10B", + "title": "Scenario 10: Event B", + "start": "2025-10-10T10:30:00Z", + "end": "2025-10-10T11:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#4ecdc4" + } + }, + { + "id": "S10C", + "title": "Scenario 10: Event C", + "start": "2025-10-10T11:30:00Z", + "end": "2025-10-10T12:30:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#ffe66d" + } + }, + { + "id": "S10D", + "title": "Scenario 10: Event D", + "start": "2025-10-10T12:00:00Z", + "end": "2025-10-10T13:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#a8e6cf" + } + }, + { + "id": "S10E", + "title": "Scenario 10: Event E", + "start": "2025-10-10T12:00:00Z", + "end": "2025-10-10T13:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#dda15e" + } } ] \ No newline at end of file diff --git a/src/managers/EventLayoutCoordinator.ts b/src/managers/EventLayoutCoordinator.ts index f817612..8d4da38 100644 --- a/src/managers/EventLayoutCoordinator.ts +++ b/src/managers/EventLayoutCoordinator.ts @@ -8,6 +8,7 @@ import { CalendarEvent } from '../types/CalendarTypes'; import { EventStackManager, EventGroup, StackLink } from './EventStackManager'; import { PositionUtils } from '../utils/PositionUtils'; +import { calendarConfig } from '../core/CalendarConfig'; export interface GridGroupLayout { events: CalendarEvent[]; @@ -35,56 +36,91 @@ export class EventLayoutCoordinator { } /** - * Calculate complete layout for a column of events + * Calculate complete layout for a column of events (recursive approach) */ public calculateColumnLayout(columnEvents: CalendarEvent[]): ColumnLayout { if (columnEvents.length === 0) { return { gridGroups: [], stackedEvents: [] }; } - // Step 1: Calculate stack levels for ALL events first (to understand overlaps) - const allStackLinks = this.stackManager.createOptimizedStackLinks(columnEvents); - - // Step 2: Find grid candidates (start together ±15 min) - const groups = this.stackManager.groupEventsByStartTime(columnEvents); - const gridGroups = groups.filter(group => { - if (group.events.length <= 1) return false; - group.containerType = this.stackManager.decideContainerType(group); - return group.containerType === 'GRID'; - }); - - // Step 3: Build grid group layouts const gridGroupLayouts: GridGroupLayout[] = []; - const renderedEventIds = new Set(); + const stackedEventLayouts: StackedEventLayout[] = []; + const renderedEventsWithLevels: Array<{ event: CalendarEvent; level: number }> = []; + let remaining = [...columnEvents].sort((a, b) => a.start.getTime() - b.start.getTime()); - gridGroups.forEach(group => { - const gridStackLevel = this.calculateGridGroupStackLevel(group, columnEvents, allStackLinks); - const earliestEvent = group.events[0]; - const position = PositionUtils.calculateEventPosition(earliestEvent.start, earliestEvent.end); - const columns = this.allocateColumns(group.events); + // Process events recursively + while (remaining.length > 0) { + // Take first event + const firstEvent = remaining[0]; - gridGroupLayouts.push({ - events: group.events, - stackLevel: gridStackLevel, - position: { top: position.top + 1 }, - columns - }); + // Find events that could be in GRID with first event + // (start within threshold AND overlap) + const gridSettings = calendarConfig.getGridSettings(); + const thresholdMinutes = gridSettings.gridStartThresholdMinutes; - group.events.forEach(e => renderedEventIds.add(e.id)); - }); + const gridCandidates = [firstEvent]; + for (let i = 1; i < remaining.length; i++) { + const candidate = remaining[i]; + const startDiff = Math.abs(candidate.start.getTime() - firstEvent.start.getTime()) / (1000 * 60); - // Step 4: Build stacked event layouts for remaining events - const remainingEvents = columnEvents.filter(e => !renderedEventIds.has(e.id)); - const stackedEventLayouts: StackedEventLayout[] = remainingEvents.map(event => { - const stackLink = allStackLinks.get(event.id)!; - const position = PositionUtils.calculateEventPosition(event.start, event.end); + // Only add if starts within threshold AND overlaps with firstEvent + if (startDiff <= thresholdMinutes && this.stackManager.doEventsOverlap(firstEvent, candidate)) { + gridCandidates.push(candidate); + } + } - return { - event, - stackLink, - position: { top: position.top + 1, height: position.height - 3 } + // Decide: should this group be GRID or STACK? + const group: EventGroup = { + events: gridCandidates, + containerType: 'NONE', + startTime: firstEvent.start }; - }); + const containerType = this.stackManager.decideContainerType(group); + + if (containerType === 'GRID' && gridCandidates.length > 1) { + // Render as GRID + const gridStackLevel = this.calculateGridGroupStackLevelFromRendered( + gridCandidates, + renderedEventsWithLevels + ); + + const earliestEvent = gridCandidates[0]; + const position = PositionUtils.calculateEventPosition(earliestEvent.start, earliestEvent.end); + const columns = this.allocateColumns(gridCandidates); + + gridGroupLayouts.push({ + events: gridCandidates, + stackLevel: gridStackLevel, + position: { top: position.top + 1 }, + columns + }); + + // Mark all events in grid with their stack level + gridCandidates.forEach(e => renderedEventsWithLevels.push({ event: e, level: gridStackLevel })); + + // Remove all events in this grid from remaining + remaining = remaining.filter(e => !gridCandidates.includes(e)); + } else { + // Render first event as STACKED + const stackLevel = this.calculateStackLevelFromRendered( + firstEvent, + renderedEventsWithLevels + ); + + const position = PositionUtils.calculateEventPosition(firstEvent.start, firstEvent.end); + stackedEventLayouts.push({ + event: firstEvent, + stackLink: { stackLevel }, + position: { top: position.top + 1, height: position.height - 3 } + }); + + // Mark this event with its stack level + renderedEventsWithLevels.push({ event: firstEvent, level: stackLevel }); + + // Remove only first event from remaining + remaining = remaining.slice(1); + } + } return { gridGroups: gridGroupLayouts, @@ -93,33 +129,42 @@ export class EventLayoutCoordinator { } /** - * Calculate stack level for a grid group based on what it overlaps OUTSIDE the group + * Calculate stack level for a grid group based on already rendered events */ - private calculateGridGroupStackLevel( - group: EventGroup, - allEvents: CalendarEvent[], - stackLinks: Map + private calculateGridGroupStackLevelFromRendered( + gridEvents: CalendarEvent[], + renderedEventsWithLevels: Array<{ event: CalendarEvent; level: number }> ): number { - const groupEventIds = new Set(group.events.map(e => e.id)); - - // Find all events OUTSIDE this group - const outsideEvents = allEvents.filter(e => !groupEventIds.has(e.id)); - - // Find the highest stackLevel of any event that overlaps with ANY event in the grid group + // Find highest stack level of any rendered event that overlaps with this grid let maxOverlappingLevel = -1; - for (const gridEvent of group.events) { - for (const outsideEvent of outsideEvents) { - if (this.stackManager.doEventsOverlap(gridEvent, outsideEvent)) { - const outsideLink = stackLinks.get(outsideEvent.id); - if (outsideLink) { - maxOverlappingLevel = Math.max(maxOverlappingLevel, outsideLink.stackLevel); - } + for (const gridEvent of gridEvents) { + for (const rendered of renderedEventsWithLevels) { + if (this.stackManager.doEventsOverlap(gridEvent, rendered.event)) { + maxOverlappingLevel = Math.max(maxOverlappingLevel, rendered.level); } } } - // Grid group should be one level above the highest overlapping event + return maxOverlappingLevel + 1; + } + + /** + * Calculate stack level for a single stacked event based on already rendered events + */ + private calculateStackLevelFromRendered( + event: CalendarEvent, + renderedEventsWithLevels: Array<{ event: CalendarEvent; level: number }> + ): number { + // Find highest stack level of any rendered event that overlaps with this event + let maxOverlappingLevel = -1; + + for (const rendered of renderedEventsWithLevels) { + if (this.stackManager.doEventsOverlap(event, rendered.event)) { + maxOverlappingLevel = Math.max(maxOverlappingLevel, rendered.level); + } + } + return maxOverlappingLevel + 1; }