From d5199682e355c44457bdcd6ef6c0f5d98fae8501 Mon Sep 17 00:00:00 2001 From: ectasy663 Date: Fri, 10 Jul 2026 12:36:56 +0530 Subject: [PATCH] feat: complete UI tasks, backend optimizations, coupon admin, user kundli --- backend/main.py | 73 ++++++- backend/models.py | 18 ++ backend_logs.txt | Bin 0 -> 74730 bytes full_logs.txt | Bin 0 -> 559818 bytes src/app/admin/coupons/page.tsx | 155 ++++++++++++++ src/app/admin/layout.tsx | 3 +- src/app/admin/students/[studentId]/page.tsx | 202 ++++++++++++++++++ src/app/admin/students/page.tsx | 8 +- src/app/cbt/[examId]/page.tsx | 84 +++++++- .../cbt/history/[attemptId]/page.tsx | 6 +- src/app/dashboard/cbt/page.tsx | 164 +++++++++++--- src/app/dashboard/dpp/page.tsx | 102 +++++++-- src/app/dashboard/layout.tsx | 26 ++- src/app/dashboard/page.tsx | 5 +- src/app/upgrade/page.tsx | 10 +- src/components/AI/MistakeHeatmap.tsx | 4 +- 16 files changed, 782 insertions(+), 78 deletions(-) create mode 100644 backend_logs.txt create mode 100644 full_logs.txt create mode 100644 src/app/admin/coupons/page.tsx create mode 100644 src/app/admin/students/[studentId]/page.tsx diff --git a/backend/main.py b/backend/main.py index 4f96c04..aa23c3f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -147,6 +147,14 @@ def submit_test_attempt(payload: Dict[str, Any], session: Session = Depends(get_ attempt_id_str = payload.get("id") import uuid as _uuid attempt_id = _uuid.UUID(attempt_id_str) if attempt_id_str else _uuid.uuid4() + + # Check if this is a retake + existing_attempts_query = select(models.Attempt).where( + models.Attempt.user_id == user_id, + models.Attempt.source_id == source_id + ) + existing_attempts = session.exec(existing_attempts_query).all() + is_retake = len(existing_attempts) > 0 attempt_record = models.Attempt( id=attempt_id, @@ -160,7 +168,8 @@ def submit_test_attempt(payload: Dict[str, Any], session: Session = Depends(get_ score=0, total_questions=total_questions, accuracy=0.0, - status="Submitted" + status="Submitted", + is_retake=is_retake ) session.add(attempt_record) session.flush() @@ -522,7 +531,8 @@ async def upload_user_avatar(user_id: UUID, file: UploadFile = File(...), sessio contents = await file.read() file_extension = file.filename.split(".")[-1] if "." in file.filename else "jpg" - object_name = f"avatar_{user_id}.{file_extension}" + current_year = datetime.utcnow().year + object_name = f"avatars/{current_year}/avatar_{user_id}.{file_extension}" public_url = upload_file(contents, object_name, content_type=file.content_type) if not public_url: @@ -579,7 +589,8 @@ async def upload_admin_test( from worker import process_pdf_test pdf_bytes = await file.read() - object_name = f"tests/{uuid.uuid4()}_{file.filename}" + current_year = datetime.utcnow().year + object_name = f"tests/{current_year}/{uuid.uuid4()}_{file.filename}" url = storage.upload_file(pdf_bytes, object_name, content_type="application/pdf") if not url: raise HTTPException(status_code=500, detail="Failed to upload PDF to MinIO") @@ -739,4 +750,58 @@ def delete_test(test_id: UUID, session: Session = Depends(get_session)): # 7. Delete the Test itself session.delete(test_obj) session.commit() - return {"status": "success", "message": "Test and all associated questions, attempts, and mistakes deleted successfully"} \ No newline at end of file + return {"status": "success", "message": "Test and all associated questions, attempts, and mistakes deleted successfully"} + +# --- COUPON CRUD ROUTES --- + +class CouponCreate(BaseModel): + code: str + discount_percent: float + max_discount_amount: float + is_active: bool = True + expires_at: Optional[datetime] = None + +class CouponUpdate(BaseModel): + code: Optional[str] = None + discount_percent: Optional[float] = None + max_discount_amount: Optional[float] = None + is_active: Optional[bool] = None + expires_at: Optional[datetime] = None + +@app.get("/api/admin/coupons", tags=["Admin"]) +def get_admin_coupons(session: Session = Depends(get_session)): + coupons = session.exec(select(models.Coupon)).all() + return coupons + +@app.post("/api/admin/coupons", tags=["Admin"]) +def create_coupon(payload: CouponCreate, session: Session = Depends(get_session)): + coupon = models.Coupon(**payload.dict()) + session.add(coupon) + session.commit() + session.refresh(coupon) + return coupon + +@app.put("/api/admin/coupons/{coupon_id}", tags=["Admin"]) +def update_coupon(coupon_id: UUID, payload: CouponUpdate, session: Session = Depends(get_session)): + coupon = session.get(models.Coupon, coupon_id) + if not coupon: + raise HTTPException(status_code=404, detail="Coupon not found") + + update_data = payload.dict(exclude_unset=True) + for key, value in update_data.items(): + setattr(coupon, key, value) + + session.add(coupon) + session.commit() + session.refresh(coupon) + return coupon + +@app.delete("/api/admin/coupons/{coupon_id}", tags=["Admin"]) +def delete_coupon(coupon_id: UUID, session: Session = Depends(get_session)): + coupon = session.get(models.Coupon, coupon_id) + if not coupon: + raise HTTPException(status_code=404, detail="Coupon not found") + + session.delete(coupon) + session.commit() + return {"status": "success", "message": "Coupon deleted successfully"} \ No newline at end of file diff --git a/backend/models.py b/backend/models.py index 7ad5384..365e656 100644 --- a/backend/models.py +++ b/backend/models.py @@ -119,6 +119,23 @@ class Test(SQLModel, table=True): is_active: bool = Field(default=True) created_by: UUID +class Transaction(SQLModel, table=True): + id: UUID = Field(default_factory=uuid4, primary_key=True) + user_id: UUID = Field(foreign_key="user.id") + amount: float + tier: str + status: str + created_at: datetime = Field(default_factory=datetime.utcnow) + +class Coupon(SQLModel, table=True): + id: UUID = Field(default_factory=uuid4, primary_key=True) + code: str = Field(unique=True, index=True) + discount_percent: float + max_discount_amount: float + is_active: bool = Field(default=True) + expires_at: Optional[datetime] = None + created_at: datetime = Field(default_factory=datetime.utcnow) + class Attempt(SQLModel, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True) user_id: UUID = Field(foreign_key="user.id") @@ -132,6 +149,7 @@ class Attempt(SQLModel, table=True): total_questions: int accuracy: float = Field(default=0.0) status: str = Field(default="In_Progress") # In_Progress, Submitted, Timed_Out + is_retake: bool = Field(default=False) class Mistake(SQLModel, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True) diff --git a/backend_logs.txt b/backend_logs.txt new file mode 100644 index 0000000000000000000000000000000000000000..c04e89a30f4af1779c031ed096efde4069703b14 GIT binary patch literal 74730 zcmeI5ZEqX75y$zuK)-`vphyeEilyk$KwsppPVR7bP0-|UFN#8tWg{`-r?cd1gWi4h zYyW@pXs$H0Rx#JRx4Q#Dmb@f~v*c>|+u?BdzhAG-jhUPGc(2Tv`N^E(bz^4c7)KW7 zBmTQIC#Hkf&v^IE0p26CkI%9FJH^qS8JI)!+zjpCk$GySW`gvN`P%&5yfm-zZE1cq z=a6|0sUINq!hAAaEB!s{y~MY!Il`y^uVem!a^K?hg?VAVG~bv%nm?G&)7IC1i2D7j z2fg~Js|O9cIEyYm52617UPGjGa8@0&XI?>@clh=UI$uM>XXX~?z_wZ9`!RB#LdF

|d}tJNNkv6T<- z`T9X;y-OdB(MLRA_MUI0hxE}GXr)(HLdX0Wt#pC5I>+Y%@0Iz>1Nv-t?MJqaQnY^y z?Z3tQOWV_XcRjp^o;b5t{R{IJE#!NCW{*C_k@dX1n&(J6N6Kc2kS5Dr0eS!RiTzJ(6FPpqKH!v4ys zos@EaUMY74U2bqUIJHtw&3~+JQf54#l*jWrK_A`X45gH8sw}&-_pB?l_IadiHdU5& zzl2Ybay~_tVLS1D9P5dYCd=^-&O4MmKc5mSbAwX6FV9fc-Rn26;T2@A=aF)jXxU@* z`oiYDhW2On+P+_EUWr&8)Z`S}ezaHG73$ow3Z!{`DoL+-z}v=*bgKr(iwa1694m#(Z{$xz>9ZalHHa07ry=1Jh?m9Cpgaz{x6DRo|s=AxxJo4 z-;|NQ3YMHe=Qgn96fD^Xy$%!ms2Y~^+K&>oSC-7)vAeh%DNA19m$5#Qb#YhbU0Yc) z@Mf%ylqEN>n;0cZHjCww?HgxADJ`B)%G;d?H=itaFR3iKX=n2zSE`*YpCw0)U`f|r zB$c|cB&~;SV95dOq_j&;lKZF%mJHg{ik$MaqAaN_>1JWnE*Usc@`>6d-Hg->)?3aa z#I2Z;*J6|?TQ`spGITVu_U!my~30wl_ixWH|MVSGc`>HMe_ODoe&# zvWRiFO)N?O&pP=_I=kesRdd{9^iirGLUndYYM*+ADN8C#DoZL$Dog6xn=;nkI4ejI zBUhVPlI=wsU%l*-L(H4DVUBxf{Uyh+N~Zb%SHY4&`_wB;SyEY2SyEY2SyEZD43;cz zI4s2tQJ4UWr&8@kl>0+3}oG=C)aDDR*0L zDLb~RM#_xklCt7!YNWJyJ}GbanQC2oQ^eYvVn?n#mK@ifCD+?4)fcZ!mgIaX?R3pw z#~u2JR>``}aj$Ey>$sIAl_ixWB~fX7$#uU*Wy$a>agl?|jxSm4$kis6B&VcWjjCYD z=C8eR*R+cX#OU&AehAgrC5zHt=eTu_Tj#hpZH|)6r0r6&NuHHaO3f<~t0Qi0jLDAY zlrpzj8l~KAxuxuw#Zk(P<&v`E_C_f!o=?hCyJWes%8MO$+rpB5q)+u`E1M-5ZLuwO zN%qkcGy18Pz3N!92=|`!&hY7XBwgbBF*xkh`W=#; zCuSdS#s@vYw*fqBr%2^7#@QU<8!es_q%Le7b9|qo^fBarg<9U3H;~ywgt#7R?!Y_1 zSqxvBZ_MZKL&eg4Tk8Sp-N%0<#vW=ugl66OS&AvCqj$CbFNzW;%^QYyrB)tsl zKgaD?qa5N~{EOb8ed~`o!QTVCCz$IV;23>G!!G1+%xm*@!%uV$KhFzzj9$X0bA@q> z_i6f!X7_8DA;%m!Zt?dVwZ^_;JDyX@+_n>@l)Ejrl%2J(oEYwkzKZlsESHoO-w!jU;T-(5vb8O&E#@a&6H;E* zQcc^vv*et?s&k35D|~*$vCD|QyO;d@wZbnAt%l#B*6j0r=K;2)A7H~eg0!OzSzY`_ zPH?R3J=oVruE&ijI)Y8G?(41A+bcE3P?@%9N72q!+U&Lav_bP}fqNa*Z@pEq+N(u2 zd&ll#&pCk?n33q*MXQ+IrN}Xf!T zFXPORklk#`F^(*LBVSTt$NY@vQt>b>TQwDwilxerLBf3lO-9uiR-PJ zV3FM(2P*Ut_d`g1J}P^5zgX>+C6y(0c7Kx-CGC*Q&M;!1<5_{v3?~sgR~{Kksd*(N zPm!Z%hf?mg+){Ro5~a*oE-6b{vMDTC#JJlgmSlU? z$u3Ej^pQNtm6 z|6-IVTQ`e@?zV*`eS6hw4|d0r!Dq6m@#&pqk9{V~(3E z`Q0Ausc~PcV9B6;>eGs{q_U*4q_U*4q^`Yr>=o=}qvA%c?JUWi6`R14p}kUl@yhj= z4&%pZNW%_($=1@uGNUuJG!p_#Wvl@G~lYGr3HPH~9QhafN(F z$m0#JUg8dC#T4Jq@p*)My)oa<@c9(iuJLVxt4wolLU@NCijCq4QoqB`M)70u=i*QJ zlv>{J@RaAc8}G%kToIyp%|m~MkvSh!&82b_&26?KF75S6StZAt(I?UO2wWPFWW(5Ez4(OO|y@&C0!r1 zJ?H}0C+4Zo@7j3!p(bxpXPYau&ldW?Rltaw|zM+x}0y*A}P!?)+bH>O}a+xLWR{2FhmeIDj=h3}i@{k=A= z%wGI(r9PMQnSMZ@IL5f)>Vv)XkK$izZi&4{TVjIu>KZ?6yBoC0^h1jtXJ~=cXKAAa z#%AP9dl%lI$J>6x(U3BVW8$fKC)oGxyZ`oXEMTkLAl!}&)3x@m^rvg5O;X3Ff5&+B zg&E-}70)rEy~DV~F?UtzxA;gj#^{?iy1qo||Bhcu@L8j)8*@2sQ{HcyxBddX?HEr> zmRVkcehyxEQGC7dJx9e~5ufi)eJNd;rf3C{9833Ng;bk(&&~~Jd_3ol6SQ``Ucxdg zUqjy)U!q;U#c%x+&%XbtVA=nIUdgfI#7NPj53TkQT5?o;g1Y%^={x+4y0LuBJ1~#` z7ylpP)3^Bf1mAuz_hlPU(!Is_G=@~3f(dMi!jP?*A=@)Uc4mg`nvfsycd@4n>*gFK zJTjb2Vzf2(a0YY8x5DL%)xea3~1ihujig5?lbo6GmAWyY2R)L@FV`T=Z4 zIv@d)x-GZcnUI}Xa4xX}N}L)_{{yp~ur*vgY?yi1hT(YTwPAYEhIzw%?{cx?;af<> zxfN5z(r2^CA4#X!!1j@V6hrWt;gjNQ&K9GyiQBWb~BwR9V4RbG_1!?kX!d z2b1HWouR9&h}BKMWJUVe@`IVvTb%&4J62<3`OV6TF$jW^Vgy<88gypo%=Hq(Qih$O z#zy_|hf!OlMqGchjapZ!VYN(lgp=dH&M0`?xQezuB zlAR^x2G=gQia?z@wQ4Fm{94%JGsx{?TV)6OkEdgm9ii4xwNvwTZz?-xacSz1 zh0bJaN!f9VnebF4hS8l{>xpF~#;!^Xl^7uC_&SqH43!wv=B!4e={5tjJE_-E9bm38 zUQ%LkWrTALon@@nuBq!(x~`$?8lQnX>U21Am0j00`oFHRjTMJ9?f3jy#unto@+%wE zGg5BQ+DIkGYD7Ruoz}W4IaG3}0C&0QoMV+a z#|~zBDmipzL-kbNzE|yTX`4E02jSv6@bs}|5y`QKna#3#DoTx8=-%FA&Ow`ZT^C<< z-l4lT+|E6Cbd^I_IaXce*h$_IwutmN#9fx1cT6EWPB8PBKuVq)t(DqVRs9Ss zP6^y!oofbiVr!fKuPT_LN~&Z2w%X5Bs9@kdH?E#>x$ zExIF)Y0tp2%WHIe$@;a!A^m+@Y&YC4Y$n6qaMf0HJIMTc6eYzi)&ZO*=YH)v+smk9 z_kGWPa=m|*H*vB3$?lFiPE|c}ILmiCl2k`Y9jBJ_=d(B$q`ze67j2@YnEs`Dz6}#m--5c=E$1SRHphp1BVz?pL2@KwO#qS@d4dy;{wC+CMP< z_MgHZ>KJ;|R84AbPu5}S_mfLs)(Sj;e6}YrJV3o$PtKndFChcnby_!^8c(w2xZG36 z^AvcpM>T~HqaWjPx_3CwfWD&Wak`q$hsDxaEArpWTS-suJUL{Fzp;E~#g+Lw87Fgb zAu^4#@a335pZ?y;K;|+#uFRJ@(`(}mn7O)@K$&Mep3E=Q&C6WEP?^@AQ*edazKGOt z7%J0RpYHD?>0F3RWB1zd{g);6VKSZ5=E+npB$kG@b@V^STDI27)xK!Y0|?{Dd}i6| z&CYdRir76^k~5U{XVcS2)a-Tgd09)bm@-eYg*$V{o=M?T%6#i$%e-l~$kMVU`IhpN z%rh2C=4Cy`vxM{~!S>`iF*3J!JegmZ3(tJ8C*WAQbcFn`?oKq4cUU}S%9?aIQl@Gb z!@S(c?kuL?nZ9H_%|C-|{0=`~!N2|TWv{omXjXD9Vi`8R^DFWrdI|lcyC-$_U?-U0 zoq+=OVL!GHCVY(FJ)}9sryX27L@2M3OL=b3KK>sdbYckd7@rT3`UE*Qe+rGSE!Jx4 z+6m8%_&a|79a8qw;%_UoZH(zyb6X$oeUa{2lV0ef!bH(B@ejnhZkLTv340}G; zInKy(JTb-RrdnZyvmajJ=NsfXHl=!iyR-HB~8Fg{D1{Wa(393`&ya zG1l>-lpZcbN~GCzD07(|SLPchM44wip3E;~MVU(&D$|CnDAR_aGOgo9na+jCG@ce^ zDjz1(IbM{hTu3a9lBANPlH^CA={zLagyfp1Go+`XL-3!pgX=0!9^!e@)-~GTNv5No}L{c#a?R&SyAItKo@AY+?iXo;Cx@h;q{p4qHfm2l*+l7;CzU6y_Y7U{R?A6}I&BEO z4pefvT27WNH|uRKm$yFj-h9H5sb7|plw7HPkmvCvB|H7;RlQVobG3UHdcM?i2lCV8 zmZ7Vmtaej3nYv!w9!;|J&7O%Aj$h7NySgJm_u*9c5>fi9cG9hdOYNW2K}s@lAu^5E zK+04;Or~=>NSVro#L}pCQniz+oy@15q~uCn6`V(tvvyl|lO|bXsr=mrO>+NG|7mg? z6!{1(^t6+Ly3fcx)LzDt5=O zmAmPZtBxj_J9ioH^guW1Wqqn$TTYtX1^e!!{)e%%+qF^pDK>5BCYvk0(xlR)u5_zz zQgxH6o6Mn`Y)*15LX$C1kX_SF`Wj2!Gn}6$EnN*oH_4Xj;!3yeVf?Er-Sh*i9w((| zN~wzpEhD8yCk&NoL+ztX8-~iXZarJ3b0IQ~U(J@Oe3(q<*0W_Q7ZOXOG^sSHG?|Yk zn~+@dX>!~PyGbwWyJRHaPp?j|Cs@M+RWC8utZ9?a>1aVU0^wvD>!!`$^S z`e;q-VP%@+nHALh*c~|3;`l4n?cBfz%$e~56JEFHLzQE@vs;dE0s&9JVEk*GxlxYA z5!{|@!Sgt#l~A5XLI2#`Ys{U?1)e~nt@X-3y~ z^>&KqKF5_2=B2b<;t$7`X<^T#SkJi;+#~u3cd|at+{q0i-1QfHgd%y43%laH5-{W43%l!x{XZdLS!1hY9mwmFqzJ++sITdB$h_! z;5rA_Ie4BqcvF&VkvVuj?1Nm5b?Y24&cBCpj+moK`tnPg#kNV4JohcF^g0K3T8ClH z!R-i@-hb-GbUO#HyiarS9gI7Vp#STpA0c1*W;K}Q zxtpGvqo?LjGii4tf5il_vj$cBQSzYy7f(hfb4D?xyO%>$HH>5z)F*Y5+IT z8b9Ox8W$VzZ8XB)6U05k_kHLL_mSo?e)o{_6rXmCCC{0;#&wE4eA>tV1B6ZxdVn^y zC-a;jr}bzTovh!(oznE~xh;KoW@Mdq(aF|n@uaa-w$UznMkV=f|gd?r(!8ed$x*Nu^1pNxSZ*R_A^dTh1k@=R1cc!pD*1G;iHn z<~lD$>>hEAV=V7@Oqpk$wo&F=7hC47G^sQh(xjDK^e>ajMs>PL`rhG6cOP_tuQaXnN|Q>HN|RMmlnkXb$vuA8SR+)L zEPH^^)4aXS=X{?NPVRCoIZblSr%8I7==xdSuD-Z;C#}BnY9`5&v<(;zOYVRqyUUWa z+DLsPl_ixWl_ixW)gOlX!^q(e!?F=2*KQ&k)w85s=icfKO;SIZT4Qa{WSxJs{#Z@Y z3gHoGzneR{ed$xZo+?c$O)5<)O)5<)P3ECVCwEgNS3OPcV4Zv1iyhrw)~9NYa?>Qe zE%jeFd5FDSe9lLPB~yt%_d0j8rB|9%npB!pnpB!pnp`(cHY>U6Xp;L?c)CK@ttQJg zmb!|Xne zP3{_PSHJxOv5m%^Ug>VG^h%RTlS-3HlS-3HlR0RzImuN=liah?mz8c`V_nis?m{cc zUFfv+91cx#k6GGUaT~fx*Td4!T2h)+npB!pnpB!pn#@U)>5^*^n(V_$x0m%@+J$3j zawsd^u8nxMcz0HN|Q>HN|QNhGF@`br%CPt>HbQ$B^7#OPt|+nw41av zNgeSpbd$T-(|ribid1Wac65`j^r{zSDI9sRGL(pRGL(p%tMn+Nv?XD z+%q)U2hF6buT)QlIazWWdb%-whOwiYKFh7Q$y6F^Lo?Z2>6ImwC6y(WC6y(WC3CQ3 zbCRo$C3yxo_u%wlo!i$~_h@o!U^KbYNzJ6|VLNzV(sbWSlS-3HlS-3HlS-30X);}M zEkcv~J+PYevc7vXxjm3|Zr4Wpo!o=tOMj>|SzH&V#ZmF5IL6rV2KDht@e+UC8K6hS zI}(8R`zzf})*p0g4~|{wwjS#H z?;mIj)NHLLQ?+~TtaQ85r}_*~-K5f_(xlR)(xlR49-3@Qa@EtMwVLb)O}ZLOJ@qc9 z-6Z`k(sOwq>?Xe|{!x5hynv7L(TAV0*>A&>;17&V#U}ng!xQtV_X8iB-yq$~;uX?8 zflljPac=&f7H{$C5}$91Gt78y@cRV%uXn`-G+xKhj*W^hk?K|P4X%%IhcUEXBjmef z*3!R2+IP^5T@@3Peu^|FNPC7j&k=Tm+;58a#h;5mnRv&=YsBEyUlGU3`x9K56hGtJ zrMcgz_$N|x?m{+xjQ{)QFK2x`1>hNexnqa(1lovpQxDr*>6M(7oRyrFoRyrFoO6(~ zmt5=A!&1vj+UNs0yBa%4w&2T2&a~1UgY-!I{nx|tIjM)GH>9-Hg*NoCzVxYDWTi=^ zNu^1pNu^1p$s9DynKqX_6%$hG$LcUa6bh0zr3=CYvk0 z(xlR)(xlR)(xlR44x03mD^-hAN0Wz;?%cn=*8>|-Ut?XONv=QdgRT0os$aL#O@F7) z@M~$(KCiO^7@_z5g0=Qz{7$fYYw2oSK-~aQIS_S*>vfZCY2_Y zCY2_YCY2`FOOs7Yt~#3JNjc=nZm)EgYm7{jruWLZ(!GQFr*3jsG--9QU8TvrWX-D5 zq|&6)q|&6)q|#(gnoO5mL!-&PPT5Ubnhf{ed;u+7dFPGuFvHz8Ct&3>&QI ze)qtmr}#wAS%(PawFAWCewcmyKS1aNp$E|Z(PnXsT;e-*NaAGi7scNi-i1>p{zH@^ zjriNtO{SM#X;NuYX;NuYX;Nu2CrzeHu3^z6sU_7WFTZZ`0Ib*VT{t04ZX4?DDor*~ zdZkIFNu^1pNu^1p$$T`~gydR;Cbv3mH(4f?oet9Eq0tETc~7@(BkEM`DUzwg--d3| zmp;{&S!q&fQfX3YQfX3YG6zjIC%NirlK1Jdm-Z^9r25d#sh6aONwRO>y#v`c;%=iy zcv|lK>YiS*xza01DoH9yDoH9yDoN%b$>t}V6UK=_|UwZX3Sv?U_PmlZ^*2r(syQ?Q-zJ&$zDLfL~B2?B=7%J0Fjc`kPWDb&S zPIA?gB=6IGlJvFJHLpO2upJx*Nphc&CCSt_YJ()3E4}I@RVS%BNhL`oNhQe~B-xzg zswYX_r~4%7Ym44>QoPON)k*TC!7c2q9)eEN`2gj<^0YQ;gCu?FQ?=eol1h?Fl1h?F zl1h>}NU}M}RZo(nK1K7b zG^sSHG^sSHG^sS1lP1$8*U)H^zPZy#C8ut37o{aj_J0qzr%CE5yJj!hZ0VIIl_r%Y zl_r%Yl_vAiWD|0C5t{6}Z^^Q(Pqk>vO_MyMrvJNd!ZzyODT?LNr+UUznpB!pnpB!p znpB$1Nt5e7MbWNw_lYLEb&4W&lLye%4a08IdrYM+Hf622jg{`^O0P7jG^sSHG^sSH zG?{}Yz2r)@Fss*1(q^yQJ8-Bq^0n11z2q)dd=Ce3f+E|<`T*TRTXm5nn=8GNq>`kP zq>`kPq>^L~l59?L)sy4_NU|S0aD0uWdY#F;%1uj#q1a4%KcK0^--ce&mELNRQ?#l| zlS-3HlS-3HlS-30XtFuURZo*V&#oUd>1%8|aRoImP0}+E{R0hQKlcuv^%2Qyk&1o}_q5a6YEeq|#(QnruRH zEkctAJ+Pa!G|3w4majtWwrAej9oHS`UBSr2G zjV8&HX?ulpuXNK6um3CETiAuOgHwMV;qN|pGOdkr>n6j}ADGrkc_*2cMEPy;>aHGM z6)*7V20E@O{*H==#d&dRG#b?I+!PlGp?>40cwfA``|R{#N4Ro~5T?F{zJu{NN>6a* z+-N|icWJM1XQ0TZ*0>Me*TGH&UCl1W9~8j@0-7TZ+X(# zGrY;oz7szfJ@2I{kL_bD$2rO~!reFV%OBo#TKtG>e-s-CJ;mQ=__l#C>J#=++C4nW z8A`j0|Gf4Xe~y2aeT@IR$cgQ~5%e{8hgZnezMZtx z@$qc$bFse08#gI_{t!N^C%X85Mel!(cj6PY5#NV@6rbYzXCKm6d#OwJNAa%@ac*$W z>YHuj(I%hPMSP-IKXti%jb~%{ZSnR)u66y^ru&B(UF>MEfgH-?K`f5!$2Qh0^vhf1 zU`rb7&#t5=c%Cz~)jryeUYZ}{cMq+1YT9`lZOFDiFwf1NOW)H62%R9#0a}}OR%5jJ z2I_B}Z$vt|eTq@&Yy5l#DY4#?c5P2=IP%a|GmRz6x*aEjH_hn6{^+lG`BS~_rLvDb z>&-8xt-jyo(Ab+@juCsS%Q5QS<8sXE8(a>R?`}Cf_STM8=((SRi`esh&99Qn_r;eO zN593d&&RgusQ4Jy$P@1{P9I@3pezk#<$B+?)c(4dq@?WjlF~h?8%3Lwmg^ivDYH|p z7;??#=w)CV{4mB}&OjgI_d)Rudh$y{`A;Bw-eFAP|5Hd=lFgG3W6Knu!}sS1F^&=A z7Jo00)0fEO6>@um{Aro~0M~r&aP`x&zm5IUd?Vd)z4$r)UPDq}ukbGN4Pp&`Ybc9c z$ozgsS-ah7Pu(c|JbJRhIc zwfaMR8WsNyzizflLO;iyFHwHx@B;{(C7Pgi`27aoICGui`>&>U$$~#7l#?YW|NLjM zzMuQw!#LiY;)gw&?ZsMm{qA@4pd&Lw<;cV_V>+W>?SY*s^KGv>LMr<%P@`~^&{|<1 zU@!7Liga{=-m;0S@9^^wW#c>IbRbO0_utNCSvIENJY<#mZn?*}Z%y@^``1f;LalO? z^pc5nTphJ%^#wPs=B(Zi?GOS*oyQR^o>G}-i!=h5bjZEwVtIk7m%LQs6ZZQ|IZ+X1u?t}La);uBe+Uw`%O$Vl6FW^`q@+uNUg27H^T&o}Vqi ze&JRt%PUpx?(7A$uYF_Uaz%}OgX^{r@tJxa+heFzxdCOn6*Q8@|GY$V)0oXei(ghf3p4)?tf2Ot?t&@dwk8)N=Mdw%nZ4aPzy|p*7Ni&(sj`cu$ zM6RCEo|9wt$Hs<{JVllw3pLhIyqTpc4$HII)VYgR82OI7}v)h5ea zU#&dl_FLZ8l9G-!7H{c&*vp1<{i^-YYGr2Km1hH!yS8Nhw8P{)@(8VHuagTr+pns< zEw>oIEz>&xxnh4=Z~Kgm&GddiPURX*X&-MTLu+WKms>u^|G;~Dfj)K?SX0}5XV0-? zYXj{-Yn&0wUlSthkw$5F) zVM#gLNPB^ZidMns#Q5u4v$HqRrbgBZ1ho7n0JBI#)pShdsHSW*P@v$T}f-tt- zFHB!R=g84h?(%+m%B>o5v~S7qYPFTauvlw{;ckyFUf_6(uFfU2jU zax9-qEN)$|nU(+Sz7m#ob}U~awl`-))t(jR<9b+l&GoeKn(J}lwR6-^+HA^|a8|F( zH}~S$nCh==5uXiYEc$hhc{kd_8@Y|2Q^cXaBF?*x@!5JeqJNtmT;s|gXIu1rL=QO= z#H0@-rsit)R^Zzw9CzK^>ubcO<-!@p61!`Eg7SDzN9^Amnd{?^;HkjHq=leMUwzBx zHO8bW=Q!k8j7L_+855?ILmNa;qDW<9W{>MVF0V zSFY-~8_(N~xA9On;>JVWm>Un3qi#Ijta*xT?f9sfrWL8)WtH4QA7s`1+VVx_=lmRl z*6)jqYwx5{&$U%wWX-YdIwd1lNK$o5LaW#U>Vzo z@ojRs&}!6bMPezuzQo6OZ+6wIuuMNIX7&DD5hpQo45r^I?vAKFE2T{}Rym)TB$4^V zVwLl$^M)w%tBYOEx2n~Zc~xVS)5}p&<{;yg^N5X@GM9L)<*gC=tU>16o~jRBWX`{3 z#^BjwV>h(N^beAHBItMUZul9-g;$uB`|)dpIpA|#lOD}KHu@{>wX!SrcIVYckTapS zZB&FZ*pG77D#!T&b=U2f>+*1(vzLD}7G2e6y{*5O(Z-a1hNOJId(g(K{Xynq6dPzr+4vx!2hP?`~zsn?A@F^IYLP(StuQC`J5)@ zUx7}e+{*5myYhz7im?mxa(dqG?n*apPEy~Or;ht%es!_S`BoWF=2eYRPA>^i<{;yg^N90bmB{0z_gM)C<}< zdHG$PL3cxj@kE5w+REw8oBp+(-R1hdd6OAw%l4x9+X`0tb;QZ_ooAp2?snnX7pYo| zyjt^W=~K1l?Up{3jOy5;Jj&102bE(lrpsAghL`V>CU?tuEh{I>sn;jnmY0x=zq^c? zHU}v8pYoZmc1~Ri0w$ zPM-SWZrx>o*@etA9#7^M&NE~#VW> za}Al!g~&8I3*pYhc)f6c)3G5B+BMSa+zrOCDk-xDV=-l(JV{SyAvz0LueF6GP%pVs z^{@5J&J_caS@)UU)mZwz?{sEgh9+ri$~GScP10{1_oj7mMTfINTl!}0Ey`r2I_(i; zwbSJ*O>*^t`>kW-GBIlt43)Ivm{F#4Au^59q|#({{mZTtSuX+J)0FI0@p!V2Ev}ia zlKSE)Ri`}y)6s^)(PUGSYaUG=f+nS2yL%d7E9=RWsr;RjCRyrjSS_>6*8WH8RGn@dxis7hRvzWb`@!4_dk)=X zbCRo`Cdo1VuqW^}mTC)}mnL_io3sSnf10%C1UIoiq79n#rB9u&C{32fS;vf$d|XJJ zeAM37X)a~`IA)Y}>Oy21rAehprOE!$WOI_Mo+f$fYCmYw)fhFrX_}*)G)a~uU6FwM zO_STuyK_(eBV#q$HQi)b`gDupGEF*v7T(jJzoNVU%HD@bK)!amv-_{ye^PbZx#v>l z(x;BE^%BYIH{@zr^SeOGPnBxPU4Ey_vn91&QR@}%ZhQdN3Re{5OfL+TvpqKtl<8cE zOrtYIogwNBv0pR9W+hj>JU#%kci$E|tTCPg(e@0nevetXw5e9X`RB7d+imuI_85Cp z=;QAa`v4uw)6)J+_=kK2?~zY1Lu8NR+F-8zi*_E)-N&=b?Z%L9+X%65jm(XUyJQZ zjODHS7emW%S(jmGy)P8CIJakL@pKC7ON-}oP42wRuxF2of8uvO(>cjS`;j!cThHt~ ztC^=jcYFV0NGj=GlXFt=Hhg=~@5?Y~k|&PRBc-*lce_04>HQbuQH^J;*|X^0qgPTI zyf*ZIhHuYiJnu=mav_o`NL}ZUjyxr17Wc_GGM{;D zD9bc2MeH7NMvUbhk16w1Zy0B2dG&^|Zi{oWQFD^3jwbC%3jLr-S7W5JH2%&@lRRH$ zC^kaesrd-M`^j^pHfYk9UcF%`O)5<)O)5>gbu*ssMgQtmuNjh}RW~^@r{d+>zet*N zl8YzIrLj>xO^%WOCVsoGo3u4X&19+{)x0#h2YuZT_Ajzm(~lf2&UccDzYUu7rBAg1 z(Uopp>9%WE>J3A6la|5w*R7^YO0CmmN@A<-#Ndjl%rh2C<`t(znOi)b%ui`DKTS3# zx$0?>8ohp4r}`R8U5Ct1llCmae(zu00ZsBOjYsHld!3`na_LiPGB*pCORt($)vT&! zRW+-U2t%Ufd5-RU3(16abSv|%i!JkxQLfB07E9(8XIz|O$>vJ0G^sSHG^sSHG^sS1 zgC@P?O4Z`j(QgWr9J2oViWPPRXkI1Q)bSzo7 zhsJqA?CJ?(k|ht5+o(O3q+LR)=37}(SyEY2SyEY2Su!6>HX*r&#FDJ9)ZG#JS(4VC z!_iA}FL$a{Ynv>YUV3FoWl3d8Wl3d8Wyzc@nJ&48#gaUYD{cL>4VI+O*P+-;(x#%j zEZIcql_ixWl_ixWl_ixW^RXl)SL&MWkXW*blLYd!B+sH94olK^X{zt0_Vki0eX2#S zvZS)4vZS)4vZS(PK9;29O4Z>Ei6!^${57WRvdzzuJH}6X|MimG=k*A4qpf5bYl9_S z>FFmvP4lfRsVu21sVu21sVtd;CB5WY_ud zQ+kA_<&L8?8f%9on<~Asq_U*4q_U*4q_SilmUNOUbwzESUa~JNY3pk@S!m*OGP#asFaGc`)5Sgvd*_L))fZ>9vy$6a&jmajYdprDL;jQ{)Q zFUMcbj(Kh>PbhG2{Xz1``~zB)EyuRBy@_pjjw>VFeG|V=&E57;13%*0AH_y(O>Llb zBgFYD+Uz;{&(~-RT1Rb|{=8AD9T)er;+x_nV*G1`ezs^`@dS(1F($thv)&75S!%gA zYg+uTnQiiM&=SX{UH?1Y5sp{aXdRA@ALAW=itl6eHG2l}W7O0hdfO@b-VUxEB9zy- z-p5|KkN*buCax7pet)x;lgzc@& zv6fSrv@8Lgta^*_DU^fN>*c;ciCB#>h4i{bZ<(MMP=0wSSs&9$UKxwB@ESQ;DaP>l zeQ(jH-lC6MsY_`zE3T8OEZ=cqWl=28V~lIl;y2v+8vkQ4C}*z_YNcwm?4cz8#mE{` zMkw{GDJW?v(>c0M?xcD(4)fqOw#6z^(#zU%&a31VO5Ad8o2Gnz-0+gK+=rapdj?7H z9e%!oBwn8kY~syXL4bS%ow1+Yyu)+mo6|HU zx#q7>?VvTursQ2Wt8veIKjt!v(50;}?(s{r%FeIb+s2GCFI~3Dk_WJLO5MlUj((0k zjHOTQVak%_QPvTolpiicvdC;cl)21~EAx#rqRcZMPv#fWqRb@>m1#p-lxf3Inbr}b zOy@#m8qbR|l@F8Y95KpNE+m#lSyEY2S(59?>(`GrCAo&gl9cqRR@rT^ba^#`nOEW$_-%Qm6IUoO4-P8XsczU1xQ3j5T`NgkHiD&5gh80nJ;< zxc8ivZSIFQ##Y(pjuz z_ww59;ubzt`^d#pE>H6Pwhi)MY2?Z4<;mOFgAZ=1O6Qy(qtFG=2|`Oy$F5I=2f)rg9;%G`c@WtrC?b^RQ%7l50Lo_F=C#PjMxujFUMfKTF#E z>BC{k!(!^(7oRDdTn)%F%qrvresppOUgQ_dg-PwC|SF)STZl` zjf+0CLfagfTRfi3FU&>tlGbNiMzP+G6#*RaPd`>K?(Go(c+fx}RlHuMZ6(}tlk zt(k6bc-cvWyo`ayT4~>zP%*RNF+<9pIO@uOS;mh_Aq5h zWl3epDkn-lQI@nmMvma6V7AXk$!jr6l(id+CG(0iqRcHGPv)mA*#=8m$(63dna`4a zu$Oe4LZ2IHI;lCKw z94AFvKgs2Jqq1aMEZKzQn$MDbu$Qzf$@(Ifxt-MaeING@xa2AJ(f7fZ(R{rmSM(pD zeRevz#@$@$RWDhVFK)NCl+7+g>NhC0bsx9x#Iw9Z|Jv}XFm)>FS&DPFUhkC(|ErPy`(RF>KmynsVu2$ z+`7iCK7rIHP>#Jf%}K6#EJ=?v-S;o)>#Iv^+?FM`YrT#4XCL>@2Ybmbo)_jzpX$Y5 zSyEY2SyEY2SyEXt4@){r|5V8}pC$WXFX<$`_0`ywHEzq2JA-0L+99Osj@!{oI&06= z*^08HvZS)4vZS)4vSdD%Y{LF;K1=q&UedB8>nnAaZO*+nmL=_ppbeeg{+{*SJKIq1TJPRIBVT%5 z&sYCP>i>uwa9unoHqBZFKg*rWddAPKS?iH|x9hxP%6hDeE%T1;MwfZUV#&PXd(mZX z@pv*n-Lsy5&w6u`Yd%Z%!8X*_SC`iFtzPnQAhw~ljp*$(RoC8*Ueelz>UoiRUZkEE z>2^OVOO9Y+WVxDWX{#^IkQS9C{fz6}*q?oj8CUM}BE8&A^#VPgCHr6->g%gZzBI{_ zB&)vr(xkW0XZYPVzBKpoze{?_YUxw8s=CIlYuviVt!v!6#;t4I>s{k+T5`=}Ns>&L z*SM?omAbDZr@bV1N4c}O`@F{O+Q^-On#%im^^(=nr}i*qNo7f8No7f8NoC1AEa~KK zs^pr_lHK;D8A^Iu9;RxI+F;4P`(<}+WLJsD$$kaeVM$whyC2@|&|l>gDP>7zNo7f8 zNo7f8$viCCl;oPvlHFe8E_2FuvMpY2mOMm>X~!@Gz2pJbxS!&AQ|CwRu%s(}szt7{ zq_U*4q_U*4q_Silmb8+KyJORA%Z9>|uD-gam!v28Vdy2t#wQSU$6d6S^rhE*+`5lj z_i>X0bRYLiNN3%9a|92?$7biz5&loi&i-G~KXmVne@oifdt>!%%}K6#EV%{A-hF#X zS6|~!=_Pkj_dI=k7<+HV9X&77y^&lgpfAu=t*YuJRWGS}N!3fLUQ+dvIrNfV?xwD& z&1cDO`vfXW`om*X5b?s4%m)*U9$iQAKV zWZf!DDofr+mTXFL&C|h>Wct98w!XITG+kp!lAqH?_M0WSo=;xwnlDW|TXCgNJ&{yd zQdv@MLv=l0*YkBff8Fc(%}TDJu%wmr-P&hl^^(*r4v8f#JG=G&RlHHQqnETjEWPx~ zlFE|GlFE|GlFE`fSu$O64T~iYl5O$wuW=umHQ#>w(&YSzod3wI$*0;ox51LW^re%dfsPM_5_>Cw{*#?w`G6bCPQwOL9L(_w6NJeRXT^&92$I zZO^#uzh06%aOjPaJBQM2THDY|`qHObj$^^&TWRK29?B~>q(LoeB!C4>d{D9Qdv@2Qdv@2Qdu$w zOExFD=CLHb{dHe2>FSH#3)6I7IazWWtG!#$(s4IOzt_08jIVOL#%=elr)c(avt+sS zsU)l{sVu21sVu21sVuo}mTXpX&11=}&a-4#UtMEKdK~9&lwq)>^#N)uI{< zcJz|2^s1NCecZZ_n;dYBbK*8}TKT#8EcZ3L5P6o&4bqH=-aQ-Ac_oBOqoJ7l9h z?0QX;yTf8ho*I}|UwPNKsh8y0g~QNGT5sRnZIsqV?XaXPeX2#S>Lpb#sd`CeN!3fL zUNVnfvMI?m6qa=Lm8$bjRYgpu4t@J1jk)B8`NVC6y(WC6y(WC6y&} zu%wf_>5^+cOLpDAWLeU8i6wWj_ht`1Ci~Bl+h*^LT_xV`97~3!r^f(g$!n}i9u;rQ z%GMjyM|FMjoe7y1zZKQ>aJhzfiMVg0D`zf5rr~Do8o;WoePm^SWn0J zjjdt&FqzJ+WXm$TkXRaJNo7f8$#t`2vyy8bOCEOKUb0+YsWt&Q^^)`kbb#OfpT}+6 z$m)){cRh{9a<6fhORsC(RX&syp)6@_2}4?xwG@U*-U?|^rVT@7T1SjBoePm^JTJ;r zK1`-_#3)m_kXRaJNo7f8$viCSXUSdgWZ&&2J&jS*-nBLE zu=MFCKDoZL$DoZL$Dod`LCCid)y)3z7 zc7Al*zhqcnTOi9cPRXg4HX*r&#FDJ9RGn9TmbCuW`)x1jS(3ZfQ)#R{ zmSpKuHQ%b2RK29?B~>q}EU7G+k0qOsTti|>uAZjqyxL&NVXSepbsmAl#%4`F#Wz74 zEa^+1I$KeeRF+hhRF+hhRF=%al3sG9`nZ|LlHK<&Nu8IguWs2(?wS?TrvvZ_!;~eJC6y(WC6y(WCG)Xl6OwC4EJ=-5sx4lAmfRkUUh?41Cs6AA zs2#neE4|uF>fRgOdqWP;y*Ik|#?FfD38p;F^cW|9^1P#8(Ldy#qcHTy>{;Tur{`uT z(h<^K6+a{0C3Gv*lTa5=`RC$K#XI!P=9BQQ zIfIw|=BoG+E%isS@u8(Qu=+j1_YA#f~K0r8upGa-^UPzCbR#J5P`+r{~GV zKjG&bx!fWiPZTc4uqVgyjAMrL{}F!eDZ=HL9Fb;+o}+Yf-1D_Cx!aG)E6nA@oNc`P z*y2Yim-q5{EYp8dZ8_(?Z+v^E=ttrEw%&KbYVA|3YxB=jeJ) zzBzakl4~ASA3`=(sk*DP*lo<9Q|0@n^tu_R;Np!Teo-o{Ey-@ybnIjEPa>P2G5G&G{mI3YOd60QMWcK z{_TTS*1PYG;+=>rtE(-irTr^3@Xs+P`5HCHk!i!|{x;Am zw$@lfF79WTgTGv}K9)X$Qunjumt^&)6}cWhE&dn!VO*#Cap7Cy*yuq1JKht!c6fyE zr|7#)Y1G@)s8GwFYI)M06l|jOWhv*c<{Ichb=6q^29jII;(G3xsL zib8!%_N{n+R&T)GOKm)JiO0J|&!pbYu29s)bt@1o-|^iFW-QNRXiBEVZ@BX{{>Nfa zqd@BlD?6)C7_NH!V&n~V0(SK@o*!3Dx&Fb`&CFTM_9e)*?!%d@A=ZF_PMg8E9=Cql}A^6U-nKgHizKC|M=eBBCx%*BPs zG_%(OWG=Jg%6zSCe~r4~iiD$5nP)tn%r9IWkhz4RGHtjzAk&7SGOb%7km+2AOyk!F zWGWvf)43G_naYL4(&*ZOt{v#wL5{TpFS%0vdDM~S7W6q!qxu$StLXG4_nj_XeWmNA zb81|yzm}owD|T&k*lCtLOrF0eOP1Nz5u;=g7b00?HXq7dX2+HJ#u-uO8ILFP3u#g2 z5{AmOAuY>&h*73!Obu`$n?#skUg!lFE|Gl2uNWWJWGKhZTkA%^2q? zOIB@Isy-2D>nCeB7E9J|-1m&kEgnzix45U7s@2ZoDVMiM^j4PP$dqbh8RpfdkDBHr z*N|9}l0MZIuML)@p03~f<91pKjSphNY*yXIkl%CCj6%TgjFZ%7sXo zG<)4z<}y33%s0MvE%S`Wllg_3N100)D$|Cm*fMPxD$}}^Y?;o5$TWT(Tc+}1GM!t= zmZ@AwEKQXYW2vigWIpq>j561GDPs4CYa3&E$79MoFO0=9?Q&VO^!(>j%6#i$%e-Th zDD#ZPl6l1$QRWtpC-YmJ2$dx{2bk}xvEGAOS#qW7aOSZjHF?~PQQnu+C40$oeRWGO zN!!8t9jg6UD40wqKfNuN{_jrBCf)%96^G%96^Gy7xx+ z-mH7?jb+KEB-c<_($*JGTu$SZ{4AN@OHh7$N#CdUvC^ID4YEC!Wa*ndE2qp=>Jw-b z_$t#iZhCLJih6z0HSP!S`8sdKT53DzrI72g;rcUAuAq-eo?;Qp+pkoIF=d{0nnsy# zU2K_mZ1q~^8H*+JimzSE+~VLpWIAUR zDevV%Vrf(_sd`D(OXko^HYd5}vE)`S>?Lh|abHC0&WOBvN&3S)Fgt^X!jfFo@8aGY z*Tbw9Ipt(6Wl3d8UE@}kRF+hh%*T@SE}6lP#E) zC6y(WC6y(WC6y)fuw+w`Ybbh2S6`{8lH_N}q3pe}EV&OB>n=++QF>)bWl3d8Wl3d8 zWyyRj*@Wa85=+v%WVh@kpQ6M=*?Y5X>?QX=l3ip;Tlyy6Ncs~ZPkvjxy7R05s(67< z^z27Jexu?c&S5>pJ|$i|$L|F~?6>#DySvY4#qaP2_!ejRzB66{nfe+&lo^j>^u(mS z!jU;T<<>sIC+uS=*FQvAOKNESnB?%Wnj|&E4VuAU1t(GA4V^DLxcKAlU-8yF$Gi45-XV@BF5haecJcox z{`Dcw4enJP32eNvsb}{54Lp||7hG;%n`b@8$nf?M;m z#iw`=J~A;kaR2i7)kGP0gR zARpVNBaEZ@Pgn5$b-h+Ov$*0NB=HeOJ~xgw#}?}yM>$$|>7*{oZML;LUEv$_<(Fnw z^aRr99Yz)YKLssNsz1S)Vn>lFK8J765n_Ycp6d;=VK%DG}asY z7m!$_$ana;!1W0}yLijXF_HmKgE=#dN}lAr>MibS=anP{hT4;Mc&`gnU+$!tk5KOA z@5-nA9Azi@j!@T6&<<0SudvC|JUZtt*lnQv3_jp9ggiJy;_coVzwUd8GmtSw6X_>8+Oom`D` zmckVbTYIawW>=;C>h-x>BHE_wGaRFA+bqtk>;qlt&(td2LhjkOJl=EZk?kO(bdM2W zzh@7NwE|Z&J!7Sg7-0{0_viYwANiLdsXq00?ja4eSYeHYUiys5f#g?N+iMuBdO7EnB^^SWC-T{b>6!s3Ba-SiD76dw$kF`-NM9F6*z{ z-PsFhWB$g(*XS+KkecSx@U9AS{dlh@hdyFhsC?jn;$~|in&tR=J zrijI7dH7-UvHgjw_phOwYfj3z_HOq7YIV2H-s5YYmRGXoW41JV(ecQ&&1%nB-#=(2 zM47|2dD{bM18r@H>^NiB!K?j*RzV!6e#4VpV;z>7s&e0IPHHvVQ)cJ3TK(qgL)CVn z-B#Fc_8pJ)sx4^U=yAc%ke6@G)#|3c?~!w?EIezBaAvjcYURKGZA-ftYMH8SyV~=* z_9|Q5S(!&`k>Al?PG+oMQm{0$+H=?L0g-3}C~#k7xDL zutfG}Ry!9hWZ+LA^#O z^Zyj{J!$}GU%G)3(B5i<-at!j&J$f9pk?i7?pwcnIo-~|$B-xcu#C1Ii`f64p^ka> zquo`)J#BU$z&~KM&VAQYj4-r-zd+f!0nNK8$_4obH|JDOP)4P3Oa6>B#ugJ7TA$ z09T`?_$%+vT?FyCGT%$Y<$hF|iwluyIJ3Sr+6=4l;z(!jD07(|SLVxoxty=sISqM* zWs`Zv*DN}cHhIzHW zo3jbYRqrK&yDN6knxu`_XvrV(=?}0-bvI`>cEz*4w($fhyW;cv#oWePl-teu1*qv2 zzWKeJjwq=4X{r_CyFE#Rq`Vi!-&U9+d-i$%&wjKTVD1j@ zre93!#eXkZtEw!iENP{KvLwBLNOuHg%cRIP0GT*w`GVgGG+wL=r zl{FSi=H)yl(SF721!E=Ts(`gm^6_MTVJ>!;yy_)G|Fz5OR2N~%rX<%;Skl#3DtqVG zOLEudF!YkNq}qaAK^M;vr$<^_`c%J<%96^G%92%1lzgHr>1)k~_vSl_vtEwow~v+`jawEA+~e%96^G%96Uq zt!vyLfxXtPmuyaQ&0|SgOisZv-L{wX^_8mg%Bh#+YVV=3SRD>aj*YjZG~REIC0Y8^ z9;Pg*EU7H1EU7H1ESZlbn~+>XVo9EunYu@~4VE0n8n@;BEwFPJ?IoKly|SdTq_U*4 zq_U*4WImQ`LhcTUCF#4aOZJjhFUiw#hohI|sUF>3<7VkodziAMvZS)4vZS)4vSdD% zY(jDki6!mnV`=NB`Sp^!ST`L4OYRzbNuK1CY6INH8n-V!wa6)6r<5g?C6y(WC6y(W zC3CQ3bCPQwOVVS0_w6NZeH}oZjOHY1logz@l!nMBka7Pg=iX$ zD6A6{_@pVx%6o+eV~M1f8>&yhzG)E<~pBJ|tx-A12c| zFOo8q3yGysmQU2vE+W|S+ZPTskV4IS(59e)YJ8UjoY^owUS+1<1Uw8 z_i-yrs;_c#z%@=(;%T^g?v2$mtAEL|?;bgaQ2&yn;x9On(47U7k0mL0Qzh4sSd#UX zsx@kZCHuek#?qMe0m>5hWKb$-;&8h2BrSC&+kRF+hhRF+hh%*T?HyQ%hn^I3AYmAu$t}Z@Y@;sjy=kuW%96^G%96^G%96^GIatz5u2kEyc`SL@ z3zqctm8vz$$&&Ohxd*};#u_*G8F7vKsd?TMuR3k$C7Ua~vZS)4vZS)4vZS(P4wh_A za@Dis9^`r-^pd{5Qf&hAvZUR~J&e6K_T(6{WGe5s!IG}@*19!?gq0{+qt*C4C#wLNs-+eQv#Ex%8yWIZjXUc>)P`hFH9Igc!$&af`nfNc|=9cvXCZ zSYvb72e`(&@lM|%-aDkdLJrJriZmxkb%wC#2;)g`H^uwn&&8iiyyN1vxyyO+E8)FG4A{f)XcYe7a@=EdDoQ0)zk=O_yr|k z*(b#t-Tb3`EXnUT?El;v`A@USDz6ZDH zee8obcmpP;2eCiPTxQ3W`Nrv3<{6JC^9#=!m$`(YGHrO)xJ(;{%CzphahcAA$TX}c zJEF)`K1`<5(<7ClIexQ`C`I2tif&GF&7%98rX+8^F24UafF(>y3@|cx?TMA_T@D|pF?~6qWBt8DV*Pr zR$#R6p~Jdwx@fn5h&$`k;CDz&WrWc zrCmo(FFA}gZthvLXRwbG=SOX@q%VEyY(-g8SyEY2SyEY2SuzJpHYd62S&}==`@oXE zzEW-R^4d#sKMFk+xxL(d)=O^Pv1IywvNl-Kmp;|IzOtmUq_U*4q_U*4WDb^WPIAp- z$?l)QPQ9e7uXLM$oGiJEoj3HiIShMA>WrzEB=>i3joX)A&&^byKsrO@6noU(omQV~u;~jwN|gYg!w%p_lZfPt|;@UQ+dvs+Ux~r0OMAFPTFx*_`B> z$C5mOrTadCe0`;|cV3pMC(=mTb24%96^G%96^G%96^G z>t@MjCD%Nb-0i%7Nn2lbAGh_d<4&Jh([]) + const [loading, setLoading] = useState(true) + const [submitting, setSubmitting] = useState(false) + const [newCoupon, setNewCoupon] = useState({ + code: "", + discount_percent: 10, + max_discount_amount: 100, + }) + + useEffect(() => { + fetchCoupons() + }, []) + + const fetchCoupons = async () => { + try { + const res = await fetch('/api/admin/coupons') + const data = await res.json() + setCoupons(data) + } catch (e) { + console.error(e) + } finally { + setLoading(false) + } + } + + const handleCreate = async () => { + if (!newCoupon.code) return + setSubmitting(true) + try { + await fetch('/api/admin/coupons', { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newCoupon) + }) + setNewCoupon({ code: "", discount_percent: 10, max_discount_amount: 100 }) + await fetchCoupons() + } catch (e) { + console.error(e) + } finally { + setSubmitting(false) + } + } + + const handleDelete = async (id: string) => { + if (!confirm("Delete this coupon?")) return + try { + await fetch(`/api/admin/coupons/${id}`, { method: "DELETE" }) + await fetchCoupons() + } catch (e) { + console.error(e) + } + } + + return ( +

+
+

Coupon Management

+

Create and manage discount codes for subscriptions.

+
+ +
+ + + Create Coupon + Add a new promo code + + +
+ + setNewCoupon({...newCoupon, code: e.target.value.toUpperCase()})} + /> +
+
+ + setNewCoupon({...newCoupon, discount_percent: Number(e.target.value)})} + /> +
+
+ + setNewCoupon({...newCoupon, max_discount_amount: Number(e.target.value)})} + /> +
+ +
+
+ + + + Active Coupons + + + {loading ? ( +
+ ) : ( + + + + Code + Discount + Max Off + Status + Actions + + + + {coupons.length === 0 && ( + No coupons found. + )} + {coupons.map(c => ( + + {c.code} + {c.discount_percent}% + ₹{c.max_discount_amount} + + {c.is_active ? Active : Inactive} + + + + + + ))} + +
+ )} +
+
+
+
+ ) +} diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 2e0616a..7961399 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -1,7 +1,7 @@ "use client" import { ReactNode, useEffect } from "react" -import { LayoutDashboard, Users, Database, Terminal, Settings, FileText } from "lucide-react" +import { LayoutDashboard, Users, Database, Terminal, Settings, FileText, Tag } from "lucide-react" import Link from "next/link" import { usePathname } from "next/navigation" import { Logo } from "@/components/Logo" @@ -27,6 +27,7 @@ export default function AdminLayout({ children }: { children: ReactNode }) { } label="Question Bank" active={pathname.startsWith("/admin/questions")} /> } label="AI Agents" active={pathname.startsWith("/admin/agents")} /> } label="Students" active={pathname.startsWith("/admin/students")} /> + } label="Coupons" active={pathname.startsWith("/admin/coupons")} />
} label="System Settings" active={pathname.startsWith("/admin/settings")} /> diff --git a/src/app/admin/students/[studentId]/page.tsx b/src/app/admin/students/[studentId]/page.tsx new file mode 100644 index 0000000..a40a23c --- /dev/null +++ b/src/app/admin/students/[studentId]/page.tsx @@ -0,0 +1,202 @@ +"use client" + +import { useEffect, useState } from "react" +import { useParams, useRouter } from "next/navigation" +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { ArrowLeft, Loader2, User, Mail, Target, Award, Calendar, Activity, BookOpen } from "lucide-react" + +export default function StudentKundliPage() { + const params = useParams() + const router = useRouter() + const studentId = params.studentId as string + + const [loading, setLoading] = useState(true) + const [userData, setUserData] = useState(null) + + useEffect(() => { + // Simulated fetch of student's complete "Kundli" + setTimeout(() => { + setUserData({ + id: studentId, + name: "Rahul Sharma", + email: "rahul@example.com", + target: "NEET", + tier: "AI Pro", + joined: "Oct 12, 2025", + stats: { + total_tests: 42, + average_score: "78%", + streak: 45, + total_questions_solved: 3450, + ai_coach_interactions: 124 + }, + recent_tests: [ + { id: 1, name: "Full Syllabus NEET Mock #4", score: "620/720", date: "2 days ago", percentile: "98.5%" }, + { id: 2, name: "Physics: Kinematics DPP", score: "45/50", date: "4 days ago", percentile: "95.2%" }, + { id: 3, name: "Biology: Human Reproduction", score: "88/100", date: "1 week ago", percentile: "99.1%" } + ], + payments: [ + { id: "PAY_1", amount: "₹1599", date: "Oct 12, 2025", plan: "AI Pro (Yearly)", status: "Success" } + ], + weak_topics: ["Rotational Dynamics", "Organic Chemistry", "Genetics"] + }) + setLoading(false) + }, 800) + }, [studentId]) + + if (loading) { + return
+ } + + if (!userData) return
User not found
+ + return ( +
+ + +
+ {/* User Identity Card */} + + +
+
+ {userData.name.charAt(0)} +
+
+ {userData.name} + + {userData.email} + +
+
+
+ +
+
Target Exam
+
{userData.target}
+
+
+
Plan Tier
+ {userData.tier} +
+
+
Joined
+
{userData.joined}
+
+
+
User ID
+
{userData.id}
+
+
+
+ + {/* Stats Summary */} +
+ + +
{userData.stats.total_tests}
+
Tests Attempted
+
+
+ + +
{userData.stats.streak}
+
Day Streak
+
+
+ + +
{userData.stats.average_score}
+
Avg. Accuracy
+
+
+ + +
{userData.stats.total_questions_solved}
+
Questions Solved
+
+
+
+
+ + {/* Recent Activity & Weak Topics */} +
+ + + Recent Tests + + +
+ {userData.recent_tests.map((test: any) => ( +
+
+
{test.name}
+
{test.date}
+
+
+
{test.score}
+
{test.percentile} %ile
+
+
+ ))} +
+
+
+ + + + AI Identified Weaknesses + + +
    + {userData.weak_topics.map((topic: string, i: number) => ( +
  • +
    + {topic} +
  • + ))} +
+
+
+
+ + {/* Payment History */} + + + Payment History + + +
+ + + + + + + + + + + + {userData.payments.map((p: any) => ( + + + + + + + + ))} + +
Transaction IDPlanDateAmountStatus
{p.id}{p.plan}{p.date}{p.amount}{p.status}
+
+
+
+ +
+ ) +} diff --git a/src/app/admin/students/page.tsx b/src/app/admin/students/page.tsx index 2d84141..c9918ef 100644 --- a/src/app/admin/students/page.tsx +++ b/src/app/admin/students/page.tsx @@ -3,8 +3,10 @@ import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Search, Download, MoreVertical } from "lucide-react" +import { useRouter } from "next/navigation" export default function StudentsPage() { + const router = useRouter() const students = [ { id: "STU-001", name: "Rahul Sharma", email: "rahul@example.com", tier: "AI Pro", target: "NEET", joined: "Oct 12, 2025", tests: 42, streak: 45, lastLogin: "Today, 08:30 AM" }, { id: "STU-002", name: "Priya Patel", email: "priya@example.com", tier: "Basic", target: "JEE Main", joined: "Nov 05, 2025", tests: 14, streak: 2, lastLogin: "Yesterday, 14:15 PM" }, @@ -50,7 +52,11 @@ export default function StudentsPage() { {students.map((s) => ( - + router.push(`/admin/students/${s.id}`)} + >
{s.name}
{s.email}
diff --git a/src/app/cbt/[examId]/page.tsx b/src/app/cbt/[examId]/page.tsx index 5c9fd26..ceba32d 100644 --- a/src/app/cbt/[examId]/page.tsx +++ b/src/app/cbt/[examId]/page.tsx @@ -37,6 +37,7 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri const [showPeriodicTable, setShowPeriodicTable] = useState(false); const [isPaused, setIsPaused] = useState(false); const [showSubmitConfirm, setShowSubmitConfirm] = useState(false); + const [accessError, setAccessError] = useState(null); const [localTheme, setLocalTheme] = useState('light'); useEffect(() => { @@ -97,6 +98,20 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri const testRes = await fetch(`${apiUrl}/api/tests/${examId}?user_id=b0e17378-864d-439e-8806-708dc48a891b`); if (!testRes.ok) throw new Error("Failed to fetch test structure"); const testData = await testRes.json(); + + // Strict boundary checking + const now = new Date(); + if (testData.scheduled_date && new Date(testData.scheduled_date) > now) { + setAccessError(`This exam is scheduled for ${new Date(testData.scheduled_date).toLocaleString()}. You cannot start it yet.`); + setMounted(true); + return; + } + if (testData.end_date && new Date(testData.end_date) < now) { + setAccessError(`This exam expired on ${new Date(testData.end_date).toLocaleString()} and is no longer available.`); + setMounted(true); + return; + } + setExamType(testData.type); // Extract all question IDs from sections @@ -232,7 +247,26 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri await submitAttemptToServer('b0e17378-864d-439e-8806-708dc48a891b'); } - if (!mounted || questions.length === 0) return
Loading Exam Engine...
; + if (!mounted || questions.length === 0) { + return
Initializing CBT Engine
+ } + + if (accessError) { + return ( +
+
+
+ +
+

Access Denied

+

{accessError}

+ +
+
+ ); + } if (status === 'NOT_STARTED') { return ( @@ -293,8 +327,18 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri const currentQuestion = questions[currentQuestionIndex]; const currentState = currentQuestion ? answers[currentQuestion.id] : null + // Helper to ensure math options missing $ delimiters get rendered properly + const renderLatex = (text: string) => { + if (typeof text !== 'string') return text; + // If it contains backslash but no dollar signs, we might want to wrap the whole thing in $ $ + if (text.includes('\\') && !text.includes('$')) { + return `$${text}$`; + } + return text; + }; + const handleOptionSelect = (qId: string, letter: string, type: string) => { - if (type === 'Multiple_Choice') { + if (type === 'MCQ_Multiple' || type === 'Multiple_Choice') { const currentSelected = currentState?.selected_answer || ""; const selectedArray = currentSelected.split(',').filter(Boolean); if (selectedArray.includes(letter)) { @@ -421,6 +465,13 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri {currentQuestion.text_en}
+ + {(currentQuestion.type === 'MCQ_Multiple' || currentQuestion.type === 'Multiple_Choice') && ( +

+ + This question has MULTIPLE CORRECT OPTIONS. Please select all that apply. +

+ )}
{currentQuestion.type === 'Numerical' ? ( @@ -441,7 +492,8 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri ) : ( currentQuestion.options?.map((option: string, idx: number) => { const letter = String.fromCharCode(65 + idx); - const isSelected = currentQuestion.type === 'Multiple_Choice' + const isMultiple = currentQuestion.type === 'MCQ_Multiple' || currentQuestion.type === 'Multiple_Choice'; + const isSelected = isMultiple ? (currentState?.selected_answer || "").split(',').includes(letter) : currentState?.selected_answer === letter; @@ -453,14 +505,14 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri ${isSelected ? 'border-primary bg-primary/5 shadow-[0_0_15px_-5px_var(--color-primary)]' : 'border-border bg-background hover:border-primary/50 hover:bg-secondary/20'} `} > -
{letter}
- {option} + {renderLatex(option)} @@ -481,6 +533,14 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
+4 / -1 +
@@ -488,6 +548,13 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri {currentQuestion.text_hi}
+ + {(currentQuestion.type === 'MCQ_Multiple' || currentQuestion.type === 'Multiple_Choice') && ( +

+ + This question has MULTIPLE CORRECT OPTIONS. Please select all that apply. +

+ )}
{currentQuestion.type === 'Numerical' ? ( @@ -508,7 +575,8 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri ) : ( currentQuestion.options_hi?.map((option: string, idx: number) => { const letter = String.fromCharCode(65 + idx); - const isSelected = currentQuestion.type === 'Multiple_Choice' + const isMultiple = currentQuestion.type === 'MCQ_Multiple' || currentQuestion.type === 'Multiple_Choice'; + const isSelected = isMultiple ? (currentState?.selected_answer || "").split(',').includes(letter) : currentState?.selected_answer === letter; @@ -520,14 +588,14 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri ${isSelected ? 'border-primary bg-primary/5 shadow-[0_0_15px_-5px_var(--color-primary)]' : 'border-border bg-background hover:border-primary/50 hover:bg-secondary/20'} `} > -
{letter}
- {option} + {renderLatex(option)} diff --git a/src/app/dashboard/cbt/history/[attemptId]/page.tsx b/src/app/dashboard/cbt/history/[attemptId]/page.tsx index f1b4ee2..0979f55 100644 --- a/src/app/dashboard/cbt/history/[attemptId]/page.tsx +++ b/src/app/dashboard/cbt/history/[attemptId]/page.tsx @@ -93,15 +93,15 @@ export default function AttemptHistoryPage({ params }: { params: Promise<{ attem

Review your performance, detailed solutions, and identify your weak spots.

-
- +
+
Score
{scoreCalc} / {maxScore}
- +
Time Taken
diff --git a/src/app/dashboard/cbt/page.tsx b/src/app/dashboard/cbt/page.tsx index 42829b4..be4c65f 100644 --- a/src/app/dashboard/cbt/page.tsx +++ b/src/app/dashboard/cbt/page.tsx @@ -16,6 +16,7 @@ interface TestData { total_time: number; difficulty: string; scheduled_date?: string; + end_date?: string; } export default function CBTPage() { @@ -26,29 +27,45 @@ export default function CBTPage() { const [attempts, setAttempts] = useState([]); const [showUpgradeModal, setShowUpgradeModal] = useState(false); const [showAllExams, setShowAllExams] = useState(false); + const [selectedPattern, setSelectedPattern] = useState(""); + const [dateFilter, setDateFilter] = useState(""); const { userId, user, hasAccessTo, loading: userLoading } = useActiveUser(); useEffect(() => { if (userLoading || !userId) return; - async function loadCBTExams() { + async function loadUser() { try { - // 1. Fetch user target exam const userRes = await fetch(`/api/users/${userId}`); if (!userRes.ok) throw new Error("Failed to load user target exam"); const userData = await userRes.json(); const pattern = userData.target_exam; setTargetExam(pattern === "NEET" ? "NEET" : pattern === "JEE_Advanced" ? "JEE Advanced" : "JEE Main"); + + // Only set the initial pattern if it hasn't been changed by the user + if (!selectedPattern) { + setSelectedPattern(pattern); + } + } catch (err) { + console.error("Error loading user:", err); + } + } + loadUser(); + }, [userId, userLoading]); - // 2. Fetch tests filtered by exam - const testRes = await fetch(`/api/tests?exam_pattern=${pattern}&user_id=${userId}`); + useEffect(() => { + if (!userId || !selectedPattern) return; + + async function loadTests() { + setLoading(true); + try { + const testRes = await fetch(`/api/tests?exam_pattern=${selectedPattern}&user_id=${userId}`); if (testRes.ok) { const testsData = await testRes.json(); setExams(testsData); } - // 3. Fetch user attempts const attemptsRes = await fetch(`/api/users/${userId}/attempts`); if (attemptsRes.ok) { const attemptsData = await attemptsRes.json(); @@ -60,12 +77,21 @@ export default function CBTPage() { setLoading(false); } } - loadCBTExams(); - }, [userId, userLoading]); + loadTests(); + }, [userId, selectedPattern]); - const filteredExams = exams.filter(exam => - exam.title.toLowerCase().includes(search.toLowerCase()) - ); + const filteredExams = exams.filter(exam => { + const matchesSearch = exam.title.toLowerCase().includes(search.toLowerCase()); + if (!matchesSearch) return false; + + if (dateFilter) { + if (!exam.scheduled_date) return false; + const scheduledDate = new Date(exam.scheduled_date).toISOString().split('T')[0]; + if (scheduledDate !== dateFilter) return false; + } + + return true; + }); const formatDate = (dateStr: string) => { return new Date(dateStr).toLocaleDateString("en-US", { year: 'numeric', month: 'short', day: 'numeric' }); @@ -73,6 +99,9 @@ export default function CBTPage() { // Get the highest score for each test if attempted multiple times const attemptsByTest = attempts.reduce((acc, curr) => { + // Isolate analytics to first attempt only - ignore retakes + if (curr.is_retake) return acc; + if (!acc[curr.source_id] || acc[curr.source_id].score < curr.score) { acc[curr.source_id] = curr; } @@ -88,18 +117,45 @@ export default function CBTPage() { Real exam environment simulations to conquer {targetExam || "NEET/JEE"}.

-
- - setSearch(e.target.value)} - className="w-full pl-10 pr-4 py-2 bg-background border border-border rounded-xl text-sm focus:ring-2 focus:ring-[#00F0FF] outline-none" - /> +
+
+ setDateFilter(e.target.value)} + className="w-full px-4 py-2 bg-background border border-border rounded-xl text-sm focus:ring-2 focus:ring-[#00F0FF] outline-none text-muted-foreground" + /> +
+
+ + setSearch(e.target.value)} + className="w-full pl-10 pr-4 py-2 bg-background border border-border rounded-xl text-sm focus:ring-2 focus:ring-[#00F0FF] outline-none" + /> +
+
+ {["JEE_MAIN", "JEE_ADVANCED", "NEET"].map(p => { + // Normalize to match db/backend pattern + const dbPattern = p === "JEE_MAIN" ? "JEE_Main" : p === "JEE_ADVANCED" ? "JEE_Advanced" : "NEET"; + return ( + + ) + })} +
+

Upcoming & Active Exams @@ -122,10 +178,12 @@ export default function CBTPage() { // Date logic const now = new Date(); const scheduledTime = exam.scheduled_date ? new Date(exam.scheduled_date) : now; + const endTime = exam.end_date ? new Date(exam.end_date) : null; const isUpcoming = scheduledTime > now; + const isExpired = endTime ? now > endTime : false; return ( - +
@@ -146,6 +204,10 @@ export default function CBTPage() { Premium + ) : isExpired ? ( + + Expired + ) : isAttempted ? ( Attempted @@ -157,7 +219,7 @@ export default function CBTPage() {

- {isUpcoming ? `Available on ${formatDate(exam.scheduled_date!)}` : 'Available Now'} + {isUpcoming ? `Available on ${formatDate(exam.scheduled_date!)}` : isExpired ? `Expired on ${formatDate(exam.end_date!)}` : 'Available Now'}
{exam.total_time} Min • {exam.difficulty} Difficulty
@@ -173,10 +235,29 @@ export default function CBTPage() { > Unlock Test + ) : isExpired && !isAttempted ? ( + + ) : isAttempted ? ( +
+ + + + {!isExpired && ( + + + + )} +
) : ( - - )} @@ -207,10 +288,12 @@ export default function CBTPage() { // Date logic const now = new Date(); const scheduledTime = exam.scheduled_date ? new Date(exam.scheduled_date) : now; + const endTime = exam.end_date ? new Date(exam.end_date) : null; const isUpcoming = scheduledTime > now; + const isExpired = endTime ? now > endTime : false; return ( - +
@@ -231,6 +314,10 @@ export default function CBTPage() { Premium + ) : isExpired ? ( + + Expired + ) : isAttempted ? ( Attempted @@ -242,7 +329,7 @@ export default function CBTPage() {
- {isUpcoming ? `Available on ${formatDate(exam.scheduled_date!)}` : 'Available Now'} + {isUpcoming ? `Available on ${formatDate(exam.scheduled_date!)}` : isExpired ? `Expired on ${formatDate(exam.end_date!)}` : 'Available Now'}
{exam.total_time} Min • {exam.difficulty} Difficulty
@@ -258,10 +345,29 @@ export default function CBTPage() { > Unlock Test + ) : isExpired && !isAttempted ? ( + + ) : isAttempted ? ( +
+ + + + {!isExpired && ( + + + + )} +
) : ( - - )} diff --git a/src/app/dashboard/dpp/page.tsx b/src/app/dashboard/dpp/page.tsx index 226064e..cb81c7a 100644 --- a/src/app/dashboard/dpp/page.tsx +++ b/src/app/dashboard/dpp/page.tsx @@ -9,9 +9,11 @@ import Link from "next/link" import { useActiveUser } from "@/hooks/useActiveUser" export default function DPPPage() { - const { user } = useActiveUser() + const { user, userId } = useActiveUser() const [dpps, setDpps] = useState([]) + const [attempts, setAttempts] = useState([]) const [loading, setLoading] = useState(true) + const [dateFilter, setDateFilter] = useState("") useEffect(() => { async function fetchDpps() { @@ -25,6 +27,14 @@ export default function DPPPage() { const data = await res.json() setDpps(data) } + + if (userId) { + const attemptsRes = await fetch(`/api/users/${userId}/attempts`); + if (attemptsRes.ok) { + const attemptsData = await attemptsRes.json(); + setAttempts(attemptsData); + } + } } catch (err) { console.error("Failed to fetch DPPs", err) } finally { @@ -43,6 +53,29 @@ export default function DPPPage() { } } + const attemptsByTest = attempts.reduce((acc, curr) => { + // Isolate analytics to first attempt only - ignore retakes + if (curr.is_retake) return acc; + + if (!acc[curr.source_id] || acc[curr.source_id].score < curr.score) { + acc[curr.source_id] = curr; + } + return acc; + }, {} as Record); + + const filteredDpps = dpps.filter(dpp => { + if (dateFilter) { + if (!dpp.scheduled_date) return false; + const scheduledDate = new Date(dpp.scheduled_date).toISOString().split('T')[0]; + if (scheduledDate !== dateFilter) return false; + } + return true; + }); + + const formatDate = (dateStr: string) => { + return new Date(dateStr).toLocaleDateString("en-US", { year: 'numeric', month: 'short', day: 'numeric' }); + } + return (
@@ -56,6 +89,12 @@ export default function DPPPage() {
+ setDateFilter(e.target.value)} + className="px-3 py-1.5 bg-background border border-border rounded-lg text-xs font-bold focus:ring-2 focus:ring-[#00F0FF] outline-none text-muted-foreground mr-2" + /> Difficulty: {['Mixed', 'Easy', 'Medium', 'Hard'].map((diff) => ( - +
+ {isUpcoming ? ( + + ) : isExpired && !isAttempted ? ( + + ) : isAttempted ? ( + <> + + + + {!isExpired && ( + + + + )} + ) : (
} label="Overview" active={pathname === "/dashboard"} /> - } label="Daily Practice" active={pathname.startsWith("/dashboard/dpp")} /> - } label="Study Materials" active={pathname.startsWith("/dashboard/materials")} /> + } label="Target DPPs" active={pathname.startsWith("/dashboard/dpp")} /> } label="Syllabus Tracker" active={pathname.startsWith("/dashboard/progress")} /> } label="Leaderboard" active={pathname.startsWith("/dashboard/leaderboard")} /> @@ -73,10 +72,17 @@ export default function DashboardLayout({ children }: { children: ReactNode }) { } - label="Exams (CBT)" + label="Mock Tests (CBT)" active={pathname.startsWith("/dashboard/cbt")} isLocked={!hasAccessTo("weekly-test-series")} /> + } + label="Quick Notes" + active={pathname.startsWith("/dashboard/materials")} + isLocked={!hasAccessTo("detailed-performance-analytics")} + /> } @@ -84,13 +90,6 @@ export default function DashboardLayout({ children }: { children: ReactNode }) { active={pathname.startsWith("/dashboard/analytics")} isLocked={!hasAccessTo("detailed-performance-analytics")} /> - } - label="Rank Predictor" - active={pathname.startsWith("/dashboard/rank-predictor")} - isLocked={!hasAccessTo("college-predictor")} - /> } @@ -111,6 +110,13 @@ export default function DashboardLayout({ children }: { children: ReactNode }) { active={pathname.startsWith("/dashboard/ai-coach")} isLocked={!hasAccessTo("ai-tutor-chat")} /> + } + label="Rank Predictor" + active={pathname.startsWith("/dashboard/rank-predictor")} + isLocked={!hasAccessTo("college-predictor")} + />
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 8ef6f76..ad273a2 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -91,7 +91,10 @@ export default function DashboardPage() {
-

City Rank

+
+

City Rank

+ Pro +

#42 / 1,200

Top 5% in Jaipur

diff --git a/src/app/upgrade/page.tsx b/src/app/upgrade/page.tsx index f265922..e3effa7 100644 --- a/src/app/upgrade/page.tsx +++ b/src/app/upgrade/page.tsx @@ -109,7 +109,7 @@ export default function UpgradePage() { Free Basic access for everyone. -
₹10/month
+
₹10/year
Subject-wise diagnostics
@@ -124,11 +124,11 @@ export default function UpgradePage() { {/* Pro Tier */} - + Pro For serious aspirants. -
₹1299/month
+
₹1299/year
Everything in Free
@@ -150,11 +150,11 @@ export default function UpgradePage() {
{/* AI Coach Tier */} - + AI Coach Personalized mastery loop. -
₹1599/month
+
₹1599/year
Everything in Pro
diff --git a/src/components/AI/MistakeHeatmap.tsx b/src/components/AI/MistakeHeatmap.tsx index bb868d7..b02be62 100644 --- a/src/components/AI/MistakeHeatmap.tsx +++ b/src/components/AI/MistakeHeatmap.tsx @@ -56,10 +56,10 @@ export function MistakeHeatmap() { {row.attempts.map((errRate, i) => (
- {errRate}% + {errRate}%
))}