From 80a95ece5b091243441800edb550615501258090 Mon Sep 17 00:00:00 2001 From: Polyanka1 Date: Fri, 14 Aug 2026 13:47:11 +0300 Subject: [PATCH] fix to prod --- android/app/src/main/AndroidManifest.xml | 2 +- .../com/behappybel/behappy/MainActivity.kt | 2 +- assets/icons/fine.png | Bin 0 -> 14237 bytes ios/Flutter/Debug.xcconfig | 2 +- ios/Flutter/Profile.xcconfig | 2 +- ios/Flutter/Release.xcconfig | 2 +- lib/data/network/api_service.dart | 34 +- .../repositories/scooter_repository_impl.dart | 18 + lib/di/service_locator.dart | 12 + lib/domain/entities/fine.dart | 38 +- .../repositories/scooter_repository.dart | 2 + lib/domain/usecase/get_fines_usecase.dart | 13 + .../components/full_screen_image_viewer.dart | 18 +- lib/presentation/components/side_menu.dart | 8 + lib/presentation/event/fine_list_event.dart | 5 + lib/presentation/navigation/app_router.dart | 39 +- .../screens/card_binding_result_screen.dart | 8 +- ...tails.dart => company_details_screen.dart} | 0 .../screens/fine_list_screen.dart | 376 ++++++++++++++++++ .../screens/fine_payment_screen.dart | 325 +++++++++------ .../screens/order_history_screen.dart | 4 +- lib/presentation/screens/phone_screen.dart | 35 -- lib/presentation/screens/support_screen.dart | 6 +- lib/presentation/state/fine_list_state.dart | 31 ++ .../viewmodel/fine_list_bloc.dart | 73 ++++ .../viewmodel/fine_payment_bloc.dart | 36 +- pubspec.yaml | 2 +- 27 files changed, 872 insertions(+), 221 deletions(-) create mode 100644 assets/icons/fine.png create mode 100644 lib/domain/usecase/get_fines_usecase.dart create mode 100644 lib/presentation/event/fine_list_event.dart rename lib/presentation/screens/{company_details.dart => company_details_screen.dart} (100%) create mode 100644 lib/presentation/screens/fine_list_screen.dart create mode 100644 lib/presentation/state/fine_list_state.dart create mode 100644 lib/presentation/viewmodel/fine_list_bloc.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e6c0a2b..0a911b6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -26,7 +26,7 @@ + android:value="45fe500d-b549-4444-9e4b-3b2acd78ee2c" /> O0qHs#q)U)E8WaI(5NRZj5G0jO>4xv| z`ThR5*S&V1+1cHB<~=hzJ5P+ZmNGsrB`yE}z*kjK)CB-g^pL+?Y)oX2-#@P!Q5k6$w0005#dR2Aj)-=Q4jJA0DO)i()NRoeOVCXUw-8^?E_U^{}XfT(fA z^i3?$j|`7YM9VN@dT_`Klu_(tF_GV71;kCT`+lWL|u^7)#VKi$^;RS-dm2gU`_BLS-lCa12YqaiJNv?cI9VX2KS0W51%+K0>3~t zKs5lyan(C!nVJv*iL-f_?BEyLbz%H-7E3aBEN@ou;2fv{m`3ky*m6AM0JKfaJ% zH%aHfp`*}kADeTGFDJV~w^>k-#$i8wH66PB6?H&~fiL*c{yqy(j@{e9=6K3JRD`8b zuW(O3QO8k+^^V)`_<1S73d3f>W0+O9CuvdCUvbqx@AD>e7a1A|{1U*8*4q$Hk9*!N z`C9YzkLb_Fuv`G}fQV=^he$fCJZvu{%Lf44jtwaf;|j^be5TmIKwr6vN)HdM&kcHq zPVD*u#wmu?E5zR*F^(2Qyc%+!rYOz*0A5_FO2L_=D8M@PC4H`YgC-g5?|h#uTNj#z z8K%A?`XS!3!LwI&{S_N08wP@d;!s&p&!cNhV2-#;NpA$E)3T5=5q0Yol~?fv*HuyX z;T}^k_?4@IQu@XN0%cSG7PnUj;3K2uBetnL9hPRH_@KQuO{XO^uoI${n>DZ~k?TV4 zbZ#`ku1klqDKU+id~-<-gT(24f6I6rW)Ei?r9Ww+XUo$VFqv|rfGit zmjwi!_X)BNp;=84mQ#e|wj!dhMg5I-N@rznX?gB<>q8_sH{Z+x+d3~(up(F9OM5y z95p39X?L$^C*whBuA2{9LI6IDC_KCI#e+cxKqg6qAk%X$R$cc46$Z8g?@!f1 z^S&wTXq9ZKVjc+<0gf7x_`T4`eLFe}vG2cGAM?aQgJ358{*#Bn8B;+#Oa`@0`J0{; zVU)Z`MunZlwQnAy?tJ%kC|pApbc3;-CS*07;UWxVepFix_b=Sr!R0(^k%gFey$#^q z=eiUL(uzV01Wqx!4CQjDz?v9PL_^-ozK_0p?4lL*p7`$+J3|UFl@$xgX9c=u{SGU9 z--{&E(U%cKVwbk)g|D-W9IQe*A4?oR4#ibbGWRsK=l&;;B&(=I)mjw?aHiqm()K<}mfvx~ za{aFV4b6E;gNh5o&s||zeQeE^w=$kNw0CUWJsAQk$ z4ydQf*HMmD#YQ5(y`s6lWskcS2VKNK9;S&dNC0mk6Y7?@D#=`*QbM)hPa_>V;;ZPX z*yI6o&|bYRa3=SNG?QRU<~id;omEPv1KL7`f{O3s$NfD?=FsuC=2z+MSoGzYe}@5Y zgt-at0oL+dg`|@abPTv1)MQg^&U?SnK`~ly#N=48cjwA^dw&*peBCKdu4|nIIaKnV zwC$YXv)y6J`->Az1r>(vdXqOg^Sg@a1g z9vc!IQn(CYtJLYOyBL#h$A&<@w`Do0$qX7Os-M1@R;t@v=T5mr_s2j#kNWJgNB-+@ zV2G1I)3CoPYj-mVeioRIZ;nY5EI-%T^;2YFT|BgEk|isvAd;xmpBSO!3RNnL4R-)4 z<1!qGVugDLcE#hEjZ3QWBxyIL4ck-}(1!tUjnuW%HsGICUq=5rG0{ zSyi9z8BZTPByI*Sth(hE=0sg=iLN9$8o=&$idIs4N)@UPg}Aih79YJ7INb=-nY2Go zqGc9JH?}#XM*qBvucWurb1tpb#KZbFWvY? zD8#J!&-;uCRYU@1zAk&_a{Ny!Ekc|St_eM%t`daAVY)q?u|ua9{|gZdy*$xak)_}E zaYdei?c@v0h8z41M{NLX*lmUEzV~GL9YymuGg$;Op=s;xIGx- z`l-`w?T8)>lS9|+c(!~E+O_+nnhTbl#uGlDM7(+}P2Xg&Ux69%t_vd9@g*yx=)>HE zi!QO##*-0ju9$QIQqo(6cj~nPEF#YmJwFW}b?VHD0ZBtckiJV`< zJ@a)iUj>8PbO>hwcW0!&QrdcL+%K>z+0^6q#t^6c?o`0E>i7De%Y^BeH_^@}(`pK2 zu1zhT{XYA>7K-h-1u7oD)~RLlB!YPI{e7C4dx^(MTo7})qpb~j)9;%iu1VANk?{6Jr+(Sf5P{x z3`=y7`*c*<6+Vp2uT3+1?To)SFMH~&k@XVL6xNGv66(=XFH5VxJ*`I zf~1A^mF@j!jZ4#ac4Ye6T(j{;;ub_`>h>0M6xdx-tF4SMfCaDZOJQD=geAsmI zr*k^BGh}NDJcOxz=d3W6potP<_5N5%WnIPY_5L|RSDUk>#Y0PORyCcyF*lo@6Qc?~ z?9AH$tT@#VOlA&7h|CPK0clERrWTWx!zvZm4;chl0fs6{I`hxI5H}W1g)`TVm%P4a z8x_rIiu$-|7&G|g06nM3%1Z%$fnB4;DXjwbmR3;%8ptt70j)yCx6wQd7(HDiLg86- z_ClkUvoxP(G}E^m#;{a5;08`nI)?f$O&n?W0x46z!PMkZRjd=Wlo`ryPPeC<%!hnx z+7h#~UJqcRQjq5J)bQi_C<263bbncfBS~TdlG|C)sAs&w)j0a5BkxyI6PR#cT-s2l z5DR^jMf^m|2EN3WnyLFo|8jOik0||I_YIW1wXm5YttKw1pTrU8!*50*T*;Y&u1AfE zAkw+iUF9KVubhDA&{)e)JN>%Gmfe7NO0SAjQ`l>QxylWDePZ)!MlYOUiv=z>;dnT= zaQ{g7@3SG%J-Ju>A&2}jTav73K&8eRLL)iim+I7=<1^ziV)ggmQyOz;pELe-so;8# zbx(w{^doQgD>_X!fo?7)FgHNa=y}XBA$jnY3Dx(7nK!J8rG*B}HbI&M3W5pJ+2zx!U5V~uVmRUC#&7J_lHd=pYXwJgZ|O*#6{%)G2JWu^h8GNb*%~}6>u@F zpy?Hcahwx5qkzaut_t(BM%-$HUYxX7kX`auVqc$eMOQvvYpoFs%p=T z?fy2Z%O|%R>WI@wO}g-h40_v4pvBvj7r|RbIw!k?R+>vzPr?efoaFc%^HX&Tw)3-I zXOfn%KhOhS$9ucf)iPXkPD%h9c{#MHFZwCArCq6Pj;QYAG!jP3k~2wV>%>^l7~JI> zBQ0Wf+Ed+M#$**;qsz-GYHXA@meaD8N>A1e3JJpn`zhD~Tmxd95s5+G{w#1+H=5~$ zWfA~NHyDjEh8}L9TK?+AK#^jsRVHb5dDZOUN@^f3fQ)EpN2o2*WHoKuR(6C$=Zq&* z?MzccGX+B2yi}NyLNZN^f!bztLeI3IUZywcH)3Yt1^yczTz6UN@s6tu;^t@cwb1@` zN@vUt=1bTO2mF(Ot#EzJQaHWOAAb?4nbg7TiDPR*>Mp3{u#~nVr1!Dbe`VVUZTKq8 zngM#E&?hA;zUdGiv2v6j_--7YxE?HGh~|&WqFAz}WFz zZ_gl9x+J>8hPSe9Ct7)0DN7T<_O4sM*vQ7IgA>q-le*uQvwJ%hqryOkU7cr)UkE`R;m;^qDytFZNtiCk zxF+!HpV9UvKSzJ#u?j~@v2i4(bpl_TA@FAOMj9jJW|}~EO2zYc@n-__%*V*vYhIMM zXg4AbvDa+`MOr#o&U?y-1MFJsO}Elr%rfye`Z9eBP+@Js4g#oH_#b4@Ds67MVyI(B zh$&nfbi>~KDX16=V-{Y80_>oLK|;#QrI8NF-1kAWvi~iSvUP&GQdXH;SWrxbw}YRL z1TOku>i3(@9rSaLG0)E$QgQ*THE+ z61C@x12{CAw+7lV@!oP7HQEBC_78~}7__A6JGmGII|&gZ2}`mVGo^QzgZ=#1xlI)t zzc5y;*zu){EsJ#`4|b@?R8HH%;CF}V|MH$Sk3A$!`{+tu$|+KA#bX~5>u}06%y{WdI1UAdb{(WAdt-8=51*br4@`N{h=7vM?Yl_=k>?%UN6N?N*n2UR6sec%! z(fPB%YcC*yYc54NGba5bclIeo>1{`|n|j<)6`23XI!N@qlvh%0%X57`*tB3(bA@tu z-FBjyaX2YrMtsRU3b(k)&T*$AJNO7h7s=qgSGk>y^L@;TAB?i#kj*&?4a!lVLgX}! zzd)r@&kS047swzWy*>H<1T51hBr>O|T%L>S^!FU&Z@7gv-#M7C4=P>gQkh$u&oFaD znS|z>HHX#SXt7IbrO}PoL5jEzspkNVns(;m-zcXoj2dm4T6Lp!NR_j-AiWat=@i-{ zU|N+K3?zHQL~f=MT$j4A2mi4&y(o^cRA^9@Ion@O3f1VQ>L5e-`M(j2%kT+!9ky}h zZSwa6xnJK8xSIX13$KZ4^SV#b>?s~0dx_8&nWe7t<=@9!&&{CVI#(0z+&^*D|lkH&eC=`6$e zndx_K$pWoAM@9@a2&)oJ=9akq3_mv5v2C-%B3fX$;BRwI23nGTYQfHMeOJZU7%E?V z)Q_una}mXt)2Ll+DQDn}ka_Ry4BNARzR@fLONz0Ec}j^1hVeOn-Y^f# zbdV#S!s3c7KPOC5t$u&5CwWY}CHiO23TxLQE#4 z?%vSYZO5SsDTp`btq;pZ0hZ#5PCLYZCMILGi}+R|r-Cq5K!#c@V{O9aeEpjJpxQ6; z8e7J`=VbA!kufYi&i>BjYWlmnpNl7G>N}Z}MvewNr3h2Ld2=tyYHZoAq?rfqQM{Oh zSQ#_6!}k&lil=x*K5qU^P*3MIgLY~P1AzH@L3+5ru3yNFM6&dZ41MglDtxWa>E5>R z&cC%Oh+Wi$@oUw~IkJ2Ojq#M4Nd^O$))t(kaAW&&==mD{$;tU7_Dr$vt)w@$nB3B4 zRjxDTcj@;6+oWD)2VkY42Jv;qHdf>!il+Sh{=h(qxORDi2l8f7 z6&5u&#mRHEbSM1Yk7i89l_^1OU~^f4!h1<3Q}pfz;7aTQ+XU!LPIT}(!NY4CMrm{8 zQm;x@G$oy&AhmfCIyY#~$cW&ZaF6;GedS=PGjj7LEd6uFFwJ8l0NR!jgQY_^bi$KqlwGTGI%W@yk&^#0#qYj~ZNO0c)A24A5QboT|)>wLCf zKy3hn289|kT?Hm=ZADEviM)nm%b|@(B5T_G9IODQH3~VhTm5$=g>jrGFO()rmKYRqlb-~NcyyR>N}b?IyMOd@F$}5 zT3Xl+nSP#xK~vDs0~VN_OI}IeIsTC0!h*A8K+TT{GVyahL!cw6iYJtm`x2*`6iIGu zIMb?}T-ZM-`hJu~V zOesFA65)2XaCbUgvG|M@`vvrk$Tz{1phv-tZ!4+iV3!LhYD8&#^{9}^)`5h|se3oK zUFCGVuH3TReZx{Yk+2C{0<1s>Bd7Wl6}FS0O0uvO`z=^o#+A=I*UC((Pk8PXawG;3 zf~t75T%OZcB|1E7MONFiZ<=H0zEzXIwx@N|pSI+36b)#OO1IOQ^Qk#%n(Nt_XcD~7 zgH=XIJgEYoZ7*jJwdC>uVAIMhJry{xNuR_yGa##^_^|+gJ({&xoNVfv-H0W5`5PMa zG*c0AzXbJM@xENqCR1!Oj}E3Zj+S{R6pNT-%bZQBkPGq|Rmrd^;GCK{>#Syaq;n-j zTYzOno!GrpbQ~tuWGT}dgZTeD+N{Ht)xdXb?Fta_8#JrAt)vN}$k#yT2m8#d?ltoN zH9Mve?1{mfo^h6>kSgmFvw{xQ{JSo@QZ-c_dx0w+1;1GBv?RN(*}UyLue#nC@L#^+ z|UW=s@Akt%^jk;DB8ubcOBbr{wigBE3hjErpekD!MHK(+0JCok7 z1}8W&!)qKj<@oPPGB4(>g&XGMv%lGp{rb8Ckvjp6FI@&<^xy((WOD{P8uM0E&$YPG zXG9s@<6s22l-*shmN!9-29x5vRV@@Y{&hP6ACw3;XTucWlpdqYuq(N=iSQ*Th*#0# z;ak5w6Qwp;mZeaK^2|?dwau_tyfUA&fMd^~BZnpLZDrTzx|#=pI2E(13T;CE%Mtyl z!9BWSle8_c^T;^El{xRL59XDv@3o~Qs3aJ*>&sTCFW!7(>%H|>EU$%#6cxQzE%I(= zji~yXzsZqL-6W=VYQV%p?02{vkgzCLue*RVX7+hOkYIb5xg~)8I$oav{_%<>e_w*( zjKoBMj)r-qN4^H_iZVFI%ai}bgKQ{2pR;4Kwa8X2&04V#U(o3>KRD+I=b+M1())yv z@%JIaB|)yP^IcV|xGlQ^#(X0HzKVp_* zX;^pAp5i~T+v0E(9LRNxV5CXzT(o6+Jdw@rOR{P93$F5%e_YU;-$~MEX5Vhi4GZ)^7T8A_QfHlJ*v)3oQTn*qQvm zIJ2VWDAAiDr+1GW8w^Sd&_|c>?5?t#7j(~}<`$^s zHH;Kp!z$lVOJW-7{S|pN?}BTmsjp789vr1_6lSTeG2o~1*DfA%cAs9h@k8?)@lmov z*3~~yn+9wLm!oUVWWMB|%%Is1f9fkNZ?QimdHH7#9SCtH6D&G!@qnGH>O&aeoUf=^C%c8 zz42_!pP>Zjt=A^b^ePP)=zJv5f~cEOb~zap6CrL+n-?B*(i5A(VXo~(yA7{$Axr`% zb_b+33-Y@;sckrief)4U3kUguPk4J2sFlRqPDcb7A~OEO8*}`1tC%x3HS7bVRW!YK z)jqgDSD?S?uy7e|jhNbfv>!)d@*nTcrbVzCN!re>GFy%+bRh6wF*s1zyyR%==Kg7R zM^}E4Wl=oqC`0#R;QT4WGwskeWYt#Xj=0Zoz2!WU!cl#OTHO&{C* z<=@ildtnJ(@mWho0sM&m;^`5N1oo0-nMJ&v2)rE7lGM2YBRhtmFKP=oS9e0YX!{nX zq<4YulxeEdGK%P5C`{)}U`tRdct2SP;W`y|-}^N;U3#;3gS_dns&4penO_m-R>t`C zfF#rB72cXU!h-FC0y1YLFi!U(w7IEmd{1n6-C8AAL-dncEM08EYS>a2+bv~va;Q>e zl8_9)(22*Zsp5X!$Bh@QMhnS1bUM&5zm4GTn@sT6GnThp^h~D7A2LTrfl(J|wqLxc**r~GCeRIIW0k(!Wd=>#@tIImWLNLU%74_e z`bib}MxYz`ST3WWfd*DGSeeMNm(|~M===_~bPnxe+Z&j+_Bo@D`NEoy9eLeI?58R+ z+(!t5U`=de7rmz2QW7cwS&=c`+W%55Z~P0@mPY3;c2RImlh=^wmDd-(r?}N4=bp$Z z50E`qRix+z19?z`vc~lS5zUsQ2yB}Lxv@EY1AQq=l>?9-8}_(?Vw(j?2QF|~-?Ogq z^9Y^4?{J9otZQNJinYGJs{5Y_7p-)s)+7Xu5H8QQ*lkpz{L1SS6so;a6Wz93lN!>L zyMHe}a`n5{4HVzHEid_CN}q;HO(K}Wx=uE?y5td`G} z!i#HVdM$r?g*T5(o12`JUa>#w!2i*_?RkyS)j9^%!c>&jUUjtX$fOK&{>0ErnwFPj ze13GE7fVu@61NHtjoD*6hVtx?)U18NY^7XuE+MgWm}|I4q<41~BXb{Nc-aAOcax^3 zjhW_z$~0%`YCp32V>IsMAZ>daY_-G+l(0CEidHP;1bU5;sIDK+l6;!CFnCDuI-NUjA zTHL!Z6#xJ+(#5tIWjgmS%OQe+c`>{6KJp_4crF?F&Ss0aT};%VO;!!x&oz_J0m6b5 z%GkE_S-JcT@AJR7rV$1q!WAESF-ZZdTn#u1BRw?&dmb#O+!3w)wAP zmK8Km0G2x9IOOS}dXj03QWS{?Q0yE5G7_UdH{2cVnllQKNT zHCb~@10@*-8N{|OnO3QK&+$>XQg283_{bMm(#l(xAudYyc9RkAeMMPebu3<4McGr5(YS`qw|7yw6-@9^uU$nd@j&!@f0-#Gcyb4Q=L z0{?5(H(OF^*Q%He83Al?0nN##Ee0Atgd~7Dd3n4+I6x$$!2we+&@T78XL4R{-+yzh zC(pUsaIOg#2HiW?%aM;=Q#?r}lhMYQ1OIdG<79KAcMv%RlHEVew>CNJ{R;lnfdb*G zi9cj?8nbNdi__kfk%w$f5x8bvp+O?4u4!-!0aMH*|T_+)N zahZ&$Pk{i?;2#@%gKB#=#y$`R`tLh*>DvV)r>~|^c>o~rO@JSt@u-&Ap9x7#9+QKe z>#m)&tTBZ{Aj*W-pT+UpLPC4Cji{1HFIG0St*Q46*9m}a;QlgNl9Zdg_wqjTIile( zFH4%HI%!GtkpX2wA#3|;$SSZPhwTNhQM9)Bvf!S^JGsnxFZ$5Kf5qRNIE0?q@4=S8 zUAZ$mJD+96J-_i$%-B@MQ-V&Atbf%YlLs#RDFox$ zsDL{r@YhuCv_xs6Z!3CFpyB!=;u#*Jk#J86w#5Y)V^+9Ysx#Rv)T2`1^x{fY0{kutQo7VGlwe$v6l?$DBJ7hn`QQn#&C_X8ddd=mw7 z^CsqkFrG$eZm;u{o6_AYjX$*-2Pu5KM8oc@A+xIB*(Bi2aNU0xAU86~OGXKxTyo=W zwMAMrfLR^BeGW@#!62|sS+>uNqIn@jj>I*Gy6 z^dEQV%ziHjFJLm3r>^p6hMUPx)OF`>#6&ogGB!veG89(YukJbUn&X4 z696-~4>vK>CfC>HpTy$al5X19(5%VR@9632KPeN1oU69a#iyq!$F{1)c9R@Aq&$hd z!1(dJZV1z)xwq`U9T3-cHs7g1;zqTkDZhQ?&24#tty{#*en|~;%W*QTGDQ&06MHI# z{`4stXOOv|z-KVLFOnGav{b>~P3jrLuc?E@L#o$X!~noFtO1HQ+^Xh3zvKL5H0)un zc$JP7UZergc-`l_IIcCn0osuYr(Yz{rfPWDjnHTXu8WZfK3z%WNxTZZ4NJfdA*j#H zxbgYpxr02BSt)mH|R!ns5aV}Nm7CcRe*xna-=vSXKbzbo0djvBr-V}4^x2RE z)vh#w7tUQ}(An)?sr;m%^dYfVkaY`yX$|!_ppa8*QLHGHy|{RQ>uvIG{Cvz~=GXX6SA zlI3S^R^>|Abs1dmo)hx~hFpb3#j1+1#sl2dDY$go+S{I(qK zFN?oc^atPQ=sxC|ibo&>4T&FP+LTP&@80K6B{h0z%p3=)#P*=h!^@`~s56NYWsXd2 zLubZr{ZW@G(>X;sT>pDe-1*&BJnH7aq>Cm+vhMsqAaKTmCY z7v=(pFf>E2nOlCxx3Ky>42pdf7#qW*^r>^eatrks&-}*R+wEp8rRqng?_^14c$iM-q7;vhaNI@`5v<)i zR*De-cq>NZ_cw?#g>RUvT#~osT|NcWGv!A|1|}vj0XOzgk=s;}*7ZU0BVWKqBSN$g z0H|WnS`YeoEOEl(xN)vMpP2QnxSZ7a-l44E;l71bn(>EDW!1x+*5Eki2;mjeC>B5; zoMhy$dRZrr0X;4T3w6w$VDR8PX+U3IQ z);RgSA8PgVn*I%ER~3AiJZ<36S1EWlds%_{Brv0fchVpASt=tcCHQ3A;fKG_WW9|_%YLqrV8J&HTymfIp_@K zr_;fv!S`Mw^MAscIPt0m3os_X&~~4ilXN7G#Z)Z#wrEqy+1JQRbgL=PF>ZT~_m zd`lYm%Bs}Lkq!WmoZtYCxzEdjJVJoUL<6U_`aPaCT#8@^@KWfcP(>)!2sGcq;C=*&OqrAh_^d7TTe)1LP zJV~&<5F#~j7tgM(_o@El@X=YQEauZR!%tFUzj^zke281jllo-rER~IrO4<}jdk?>;qR#}Ym%>7_Dp@nqVV8KsyaL(Hrtiri% zmrXUN60L$o%U4R$GIG5O>$)^H4=KUXfNe=T{nsdDQ|y^-NmSAy zTa15-hr@PrbSP%DC=_nlY-N>+u>li)zr6d0#v1UE8RY0f3w;TmK7C|qA*R{V8C%H> z0GeA5HZC>X1zorjPhpf6evo97DDHT?U##WJdGyv0i5gmI&;*2xJvXR8*1wHLR0Wpc z9GFu5i+v_N{!<+o2x52F$OSo5pC%#K!)*6A#^p`2-471F_EEnZa2ZZ4?#@FdeoJt? zmZHUngh_+&YCy=_m?_-Tb#A`j8Oy;#lF8Jm7;~_NYv=keSHEA`aB6INL{?#%pa9mM z8^F|x;}h8dB|UeLp2w1#g5CzT%2$;6lr;}1fP+3KO2*#5P0FQsm5OE5N(G(L1=rcF zwVf?(8%||O6Z2Sw5x zI#>Bhjt9vFyeP$ZJFS}BvGBq1Ui~&xM#D(ndHGgCS<%&BBBr~b^w{RjcTsUXoR~73~En801!-@((>&fRq*p zWwBpu@nQin=HF%XTzQOBxy{8^z;FEp(g#g-{Df4lq>A(b3;0wkr{a-*V=$(_ALOA5 zK!b+1I=A`t6ah7<+|I83DLO!*iCKw{8m5>wkgT{uU1)sKof#h6Y8L=Xzagy6Kt?m# zI!h9_|4?^i00*H=rnZ#3FW>Dc(2v;L3^y=8dFqPEE`4YzSLR&a#7SD?#1!dS65;m* zXSk&&zwyekg#re^_?OukZTlt7zK?&gwm-drmdU^tb^TA9N=amuyMi`~BTlPp)<1~? zM#Bw}Mf$|B7s2sN0fgHwqp4Y(oaGoJQkLQOfDpB3`Gx*%?{s!Vs7HK$&ka^7mcHhb zTCuI-h7TVN-FzXqcw&VSG+t+PENUMF=CM;|r9##l3`|~k9W%mX2`*%iwMi)#XcMTO z!QrO{x`CXL9r?c#owG^&za0G?xZc7SakpueMLdY5{D$eGI3}L5!KwsaosSBxv>t2= z>t2N5wmUDor;x1`^Sv?&pwP1;E^R$ql~yDR`Ipi@#lb|A?mv@pQ-NE7D``MK7FQ?m z?{zOUw6-v#Q9tuWK_A7KrO)(4x`a9xpVK6NCPsz^xul0`e;)M9s)0Y{XcpT}LzYpRbS|IrioB@Jm<*t{J9SQ(I z7fjOE#F{7UTwNrPk1R)|;H~h$bm?%FG2UEl;D=*85f{(E$lGSZ+0KrCsH#jht*b_t zs+*8&9C>4ue?L@n2A5hjzzr$uy9MM5$#uk_QS+wP2+Hc%ZGPk@JiL@MSn75^oP6lq z(?*sCG9>|&WQfykw!@w+WZ6gTG%nsB+-}cim=MiG(2|>Gz|Q(a72klFejEzT?bWZq zxNTHozbmNg{lEI)6-r!*4tA5g^3%-ycoFxYBdL5C zi7^H7!*_p&p?mEgyC;Su2yL+&UYp6s$MUhHzpZBvh$AKdfd6xV$ov|4U7WuSrNhaO zzf_7tjY(jnU5J?_Z0nS^_`DeW{)Zh{MZW_n$ERbY9k-%z?A2Iou0ETVqU9Vz5#G~K@Whx5MT0@^=h!> getClientFines() async { + try { + final response = await _dio.get( + "$baseUrl/scooterorder/fines/client", + options: await _getAuthOptions(), + ); + + if (response.statusCode == 200 && response.data is Map) { + final List dataList = response.data['data'] ?? []; + return dataList.map((json) => Fine.fromJson(json)).toList(); + } + return []; + } on DioException catch (e) { + _handleDioError(e); + return []; + } + } + /// GET /scooterorder/{id}/fine Future getFineDetails(int orderId) async { try { final response = await _dio.get( - "$baseUrl/scooterorder/$orderId/fine", + "$baseUrl/scooterorder/fines/client/$orderId", options: await _getAuthOptions(), ); @@ -953,7 +972,8 @@ class ApiService { } } - /// PUT /scooterorder/{id}/fine + /// PUT /scooterorder/fines/client/{id} + /// {id} — это id штрафа (Fine.id), а не id заказа (Fine.orderId) Future payFine({ required int orderId, int? cardId, @@ -961,10 +981,10 @@ class ApiService { }) async { try { await _dio.put( - "$baseUrl/scooterorder/$orderId/fine", + "$baseUrl/scooterorder/fines/client/$orderId", data: { - if (cardId != null) 'cardId': cardId, - 'isBalance': isBalance, + "cardId": cardId ?? 0, + "isBalance": isBalance, }, options: await _getAuthOptions(), ); diff --git a/lib/data/repositories/scooter_repository_impl.dart b/lib/data/repositories/scooter_repository_impl.dart index 0655491..8907467 100644 --- a/lib/data/repositories/scooter_repository_impl.dart +++ b/lib/data/repositories/scooter_repository_impl.dart @@ -449,6 +449,24 @@ class ScooterRepositoryImpl extends ScooterRepository { return result; } + @override + Future>> getClientFines() async { + late final Result> result; + try { + final fines = await _apiService.getClientFines(); + result = Success(fines); + } on AuthException catch (e) { + result = Failure(AuthFailure(e.attemptsLeft)); + } on UnauthorizedException catch (e) { + result = Failure(AuthFailure(0, message: e.toString())); + } on AuthBlockException catch (e) { + result = Failure(AuthBlockFailure(e.toString())); + } catch (e) { + result = Failure(UnknownFailure("Неизвестная ошибка")); + } + return result; + } + @override Future> payFine({ required int orderId, diff --git a/lib/di/service_locator.dart b/lib/di/service_locator.dart index 85b15d4..3b61f41 100644 --- a/lib/di/service_locator.dart +++ b/lib/di/service_locator.dart @@ -94,14 +94,18 @@ import '../domain/service/device_info_service.dart'; import '../domain/usecase/activate_subscription_usecase.dart'; import '../domain/usecase/apply_promo_code_usecase.dart'; import '../domain/usecase/get_client_subscriptions_usecase.dart'; +import '../domain/usecase/get_fine_details_usecase.dart'; +import '../domain/usecase/get_fines_usecase.dart'; import '../domain/usecase/get_news_by_id_usecase.dart'; import '../domain/usecase/get_notifications_usecase.dart'; import '../domain/usecase/get_scooter_by_title_usecase.dart'; import '../domain/usecase/get_scooter_order_history_usecase.dart'; +import '../domain/usecase/pay_fine_usecase.dart'; import '../domain/usecase/remove_payment_card_usecase.dart'; import '../domain/usecase/set_main_payment_card_usecase.dart'; import '../presentation/viewmodel/auth_bloc.dart'; import '../presentation/viewmodel/edit_profile_bloc.dart'; +import '../presentation/viewmodel/fine_list_bloc.dart'; import '../presentation/viewmodel/map_bloc.dart'; import '../presentation/viewmodel/news_bloc.dart'; import '../presentation/viewmodel/notifications_bloc.dart'; @@ -391,6 +395,8 @@ Future setupDependencies() async { () => OrderHistoryBloc(getIt()), ); + getIt.registerFactory(() => FineListBloc(getIt())); + getIt.registerFactory( () => ScooterCodeBloc( getScooterByTitleUsecase: getIt(), @@ -409,6 +415,12 @@ Future setupDependencies() async { ApplyPromoCodeUsecase(getIt()), ); + getIt.registerFactory(() => GetFinesUsecase(getIt())); + + getIt.registerFactory(() => GetFineDetailsUsecase(getIt())); + + getIt.registerFactory(() => PayFineUsecase(getIt())); + // Bloc (factory, т.к. экран создаёт новый экземпляр) getIt.registerFactory( () => PromoCodeBloc(getIt()), diff --git a/lib/domain/entities/fine.dart b/lib/domain/entities/fine.dart index 929590e..19b44f1 100644 --- a/lib/domain/entities/fine.dart +++ b/lib/domain/entities/fine.dart @@ -2,8 +2,9 @@ import 'package:equatable/equatable.dart'; class Fine extends Equatable { final int id; - final String scooterNumber; - final String date; + final int orderId; // ← добавлено + final String scooterNumber; // можно оставить для совместимости, но не использовать в UI + final DateTime createdAtDateTime; // ← новое поле final String description; final double amount; final String amountPrint; @@ -12,8 +13,9 @@ class Fine extends Equatable { const Fine({ required this.id, + required this.orderId, required this.scooterNumber, - required this.date, + required this.createdAtDateTime, required this.description, required this.amount, required this.amountPrint, @@ -28,22 +30,15 @@ class Fine extends Equatable { .where((path) => path.isNotEmpty) .toList(); - // Форматирование даты из createdAt (2026-08-07T09:15:26.565Z -> 07-08-2026) - String formattedDate = ''; - if (json['createdAt'] != null) { - final parsedDate = DateTime.tryParse(json['createdAt'].toString()); - if (parsedDate != null) { - final day = parsedDate.day.toString().padLeft(2, '0'); - final month = parsedDate.month.toString().padLeft(2, '0'); - final year = parsedDate.year; - formattedDate = '$day-$month-$year'; - } - } + // Парсим createdAt как DateTime + final createdAtDateTime = DateTime.tryParse(json['createdAt'].toString()) ?? + DateTime.now(); // fallback return Fine( id: json['id'] as int? ?? 0, + orderId: json['orderId'] as int? ?? 0, scooterNumber: scooterNumber ?? '', - date: formattedDate, + createdAtDateTime: createdAtDateTime, description: json['description'] as String? ?? '', amount: (json['value'] as num?)?.toDouble() ?? 0.0, amountPrint: json['valuePrint'] as String? ?? '0,00 BYN', @@ -52,21 +47,12 @@ class Fine extends Equatable { ); } - Map toJson() { - return { - 'id': id, - 'description': description, - 'value': amount, - 'valuePrint': amountPrint, - 'isPaid': isPaid, - }; - } - @override List get props => [ id, + orderId, scooterNumber, - date, + createdAtDateTime, description, amount, amountPrint, diff --git a/lib/domain/repositories/scooter_repository.dart b/lib/domain/repositories/scooter_repository.dart index b2b123a..f1302fc 100644 --- a/lib/domain/repositories/scooter_repository.dart +++ b/lib/domain/repositories/scooter_repository.dart @@ -62,4 +62,6 @@ abstract class ScooterRepository { required bool isBalance, }); + Future>> getClientFines(); + } diff --git a/lib/domain/usecase/get_fines_usecase.dart b/lib/domain/usecase/get_fines_usecase.dart new file mode 100644 index 0000000..0030c91 --- /dev/null +++ b/lib/domain/usecase/get_fines_usecase.dart @@ -0,0 +1,13 @@ +import '../../core/result.dart'; +import '../entities/fine.dart'; +import '../repositories/scooter_repository.dart'; + +class GetFinesUsecase { + final ScooterRepository repository; + + GetFinesUsecase(this.repository); + + Future>> call() { + return repository.getClientFines(); + } +} \ No newline at end of file diff --git a/lib/presentation/components/full_screen_image_viewer.dart b/lib/presentation/components/full_screen_image_viewer.dart index 5b00b7d..a6a45ea 100644 --- a/lib/presentation/components/full_screen_image_viewer.dart +++ b/lib/presentation/components/full_screen_image_viewer.dart @@ -23,6 +23,11 @@ class _FullScreenImageViewerState extends State { _pageController = PageController(initialPage: widget.initialIndex); } + String _resolveImageUrl(String path) { + if (path.startsWith('http://') || path.startsWith('https://')) return path; + return 'https://sharing-api.sparkit.by/$path'; + } + @override Widget build(BuildContext context) { return Scaffold( @@ -35,12 +40,17 @@ class _FullScreenImageViewerState extends State { controller: _pageController, itemCount: widget.photoUrls.length, itemBuilder: (context, index) { - final isNetwork = widget.photoUrls[index].startsWith('http'); + final photo = widget.photoUrls[index]; + final imageUrl = _resolveImageUrl(photo); return InteractiveViewer( child: Center( - child: isNetwork - ? Image.network(widget.photoUrls[index]) - : Image.asset(widget.photoUrls[index]), + child: Image.network( + imageUrl, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return const Icon(Icons.error, color: Colors.white, size: 64); + }, + ), ), ); }, diff --git a/lib/presentation/components/side_menu.dart b/lib/presentation/components/side_menu.dart index b25a24f..9ecf5aa 100644 --- a/lib/presentation/components/side_menu.dart +++ b/lib/presentation/components/side_menu.dart @@ -73,6 +73,14 @@ class SideMenu extends StatelessWidget { context.go('/home/profile'); }, ), + _MenuItem( + icon: 'assets/icons/fine.png', + title: 'Штрафы', + onTap: () { + Navigator.pop(context); + context.go('/home/fine-list'); + }, + ), _MenuItem( icon: 'assets/icons/list_star_icon.png', title: 'История поездок', diff --git a/lib/presentation/event/fine_list_event.dart b/lib/presentation/event/fine_list_event.dart new file mode 100644 index 0000000..17a3def --- /dev/null +++ b/lib/presentation/event/fine_list_event.dart @@ -0,0 +1,5 @@ +abstract class FineListEvent {} + +class FetchFinesRequested extends FineListEvent {} + +class RefreshFinesRequested extends FineListEvent {} \ No newline at end of file diff --git a/lib/presentation/navigation/app_router.dart b/lib/presentation/navigation/app_router.dart index f54c9b9..e85c0cd 100644 --- a/lib/presentation/navigation/app_router.dart +++ b/lib/presentation/navigation/app_router.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:be_happy/presentation/screens/card_binding_result_screen.dart'; -import 'package:be_happy/presentation/screens/company_details.dart'; import 'package:be_happy/presentation/screens/lease_agreement_screen.dart'; import 'package:be_happy/presentation/screens/payment_rules_screen.dart'; import 'package:bot_toast/bot_toast.dart'; @@ -79,6 +78,9 @@ import '../event/subscription_details_event.dart'; import '../event/tariff_sheet_event.dart'; import '../event/top_up_event.dart'; import '../screens/add_card_screen.dart'; // ← новый импорт +import '../screens/company_details_screen.dart'; +import '../screens/fine_list_screen.dart'; +import '../screens/fine_payment_screen.dart'; import '../screens/license_agreement_screen.dart'; import '../screens/notifications_screen.dart'; import '../screens/order_history_screen.dart'; @@ -545,6 +547,41 @@ class AppRouter { builder: (context, state) => const OrderHistoryScreen(), routes: [] ), + GoRoute( + path: 'fine-list', + builder: (context, state) => const FineListScreen(), + routes: [] + ), + GoRoute( + path: 'fine-payment/:fineId', + builder: (context, state) { + final fineId = int.parse(state.pathParameters['fineId']!); + return FinePaymentScreen(fineId: fineId); + }, + routes: [ + GoRoute( + path: 'result/:status', + builder: (context, state) { + final modeParam = state.pathParameters['status']; + + CardBindingStatus status = CardBindingStatus.success; + String title = 'Штраф успешно\nоплачен!'; + + if (modeParam == 'failed') { + status = CardBindingStatus.failed; + title = 'Извините, что-то\nпошло не так'; + } + + return CardBindingResultScreen( + status: status, + title: title, + cardImagePath: 'assets/icons/card_result_screen.png', + redirectPath: '/home/fine-list', + ); + }, + ), + ], + ), GoRoute( path: 'notifications', builder: (context, state) => const NotificationsScreen(), diff --git a/lib/presentation/screens/card_binding_result_screen.dart b/lib/presentation/screens/card_binding_result_screen.dart index 5790e71..561f16f 100644 --- a/lib/presentation/screens/card_binding_result_screen.dart +++ b/lib/presentation/screens/card_binding_result_screen.dart @@ -10,12 +10,14 @@ class CardBindingResultScreen extends StatefulWidget { final CardBindingStatus status; final String title; final String cardImagePath; + final String redirectPath; // ← новое поле const CardBindingResultScreen({ super.key, required this.status, required this.title, required this.cardImagePath, + this.redirectPath = '/home/payment-methods', // сохраняем текущее поведение по умолчанию }); @override @@ -32,7 +34,7 @@ class _CardBindingResultScreenState extends State { _autoPopTimer = Timer(const Duration(seconds: 5), () { if (mounted) { final timestamp = DateTime.now().millisecondsSinceEpoch; - context.go("/home/payment-methods?refreshId=$timestamp"); + context.go("${widget.redirectPath}?refreshId=$timestamp"); } }); } @@ -128,7 +130,7 @@ class _CardBindingResultScreenState extends State { text: "Попробовать снова", onTap: () { final timestamp = DateTime.now().millisecondsSinceEpoch; - context.go("/home/payment-methods?refreshId=$timestamp"); + context.go("${widget.redirectPath}?refreshId=$timestamp"); }, enabled: true, showArrows: true, @@ -142,7 +144,7 @@ class _CardBindingResultScreenState extends State { onPressed: () { final timestamp = DateTime.now().millisecondsSinceEpoch; - context.go("/home/payment-methods?refreshId=$timestamp"); + context.go("${widget.redirectPath}?refreshId=$timestamp"); }, style: TextButton.styleFrom( shape: RoundedRectangleBorder( diff --git a/lib/presentation/screens/company_details.dart b/lib/presentation/screens/company_details_screen.dart similarity index 100% rename from lib/presentation/screens/company_details.dart rename to lib/presentation/screens/company_details_screen.dart diff --git a/lib/presentation/screens/fine_list_screen.dart b/lib/presentation/screens/fine_list_screen.dart new file mode 100644 index 0000000..8a28413 --- /dev/null +++ b/lib/presentation/screens/fine_list_screen.dart @@ -0,0 +1,376 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import '../../core/app_colors.dart'; +import '../../di/service_locator.dart'; +import '../../domain/entities/fine.dart'; +import '../components/custom_app_bar.dart'; +import '../components/gradient_button.dart'; +import '../event/fine_list_event.dart'; +import '../state/fine_list_state.dart'; +import '../viewmodel/fine_list_bloc.dart'; +import 'fine_payment_screen.dart'; + +class FineListScreen extends StatelessWidget { + const FineListScreen({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => getIt()..add(FetchFinesRequested()), + child: const FineListView(), + ); + } +} + +class FineListView extends StatelessWidget { + const FineListView({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg), + child: SafeArea( + child: Column( + children: [ + const SizedBox(height: 16), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 20), + child: CustomAppBar(title: 'Штрафы'), + ), + const SizedBox(height: 16), + Expanded( + child: BlocBuilder( + builder: (context, state) { + if (state.status == FineListStatus.loading && state.fines.isEmpty) { + return const Center( + child: CircularProgressIndicator(color: Colors.white), + ); + } + + if (state.status == FineListStatus.failure) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Ошибка загрузки', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + const SizedBox(height: 8), + Text( + state.errorMessage ?? '', + style: TextStyle(color: Colors.white.withOpacity(0.6), fontSize: 14), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + context.read().add(FetchFinesRequested()); + }, + child: const Text('Повторить'), + ), + ], + ), + ); + } + + if (state.status == FineListStatus.empty) { + return const _EmptyState(); + } + + final groupedFines = _groupByMonth(state.fines); + + return RefreshIndicator( + onRefresh: () async { + context.read().add(RefreshFinesRequested()); + }, + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 20), + itemCount: groupedFines.length, + itemBuilder: (context, index) { + final monthKey = groupedFines.keys.elementAt(index); + final fines = groupedFines[monthKey]!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text( + _getMonthTitle(monthKey), + style: TextStyle( + color: Colors.white.withOpacity(0.7), + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ...fines.map((fine) => _FineCard(fine: fine)), + ], + ); + }, + ), + ); + }, + ), + ), + ], + ), + ), + ), + ); + } + + Map> _groupByMonth(List fines) { + final Map> grouped = {}; + + for (var fine in fines) { + final localDate = fine.createdAtDateTime.toLocal(); + final monthKey = '${localDate.year}-${localDate.month.toString().padLeft(2, '0')}'; + + grouped.putIfAbsent(monthKey, () => []); + grouped[monthKey]!.add(fine); + } + + final sortedKeys = grouped.keys.toList()..sort((a, b) => b.compareTo(a)); + + return Map.fromEntries( + sortedKeys.map((key) => MapEntry(key, grouped[key]!)), + ); + } + + String _getMonthTitle(String monthKey) { + final parts = monthKey.split('-'); + final year = int.parse(parts[0]); + final month = int.parse(parts[1]); + + const months = [ + 'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', + 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь' + ]; + + return '${months[month - 1]} $year'; + } +} + +class _EmptyState extends StatelessWidget { + const _EmptyState(); + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/history.png', + width: 380, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return Container( + width: 280, + height: 280, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: const Icon( + Icons.warning_amber_outlined, + size: 120, + color: Colors.white38, + ), + ); + }, + ), + const SizedBox(height: 40), + const Text( + 'У вас пока нет штрафов.', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.4, + ), + ), + const SizedBox(height: 32), + GradientButton( + text: 'Прокатиться', + showArrows: true, + height: 56, + width: double.infinity, + fontSize: 16, + onTap: () { + context.go('/home'); + }, + ), + ], + ), + ), + ); + } +} + +// 🔹 КАРТОЧКА ШТРАФА — полностью как _OrderCard, но с Fine +class _FineCard extends StatelessWidget { + final Fine fine; + + const _FineCard({required this.fine}); + + @override + Widget build(BuildContext context) { + final date = _formatDateFromFineDate(fine.createdAtDateTime); // преобразуем "16-06-2025" → "16 июня, 15:48" + final orderId = '${fine.orderId}'; + final price = fine.amountPrint; + + + return GestureDetector( + onTap: () { + context.push('/home/fine-payment/${fine.id}'); }, + child: Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.fromLTRB(4, 4, 16, 4), + decoration: BoxDecoration( + color: const Color(0xFF0A0F2E).withOpacity(0.7), + borderRadius: BorderRadius.circular(16), + ), + child: Row( + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.asset( + 'assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png', + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return const Icon( + Icons.warning_amber, + color: Colors.white, + size: 48, + ); + }, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + date, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.qr_code_2, + color: Colors.white.withOpacity(0.6), + size: 18, + ), + const SizedBox(width: 4), + Text( + orderId, + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 15, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ), + ), + // Столбец 3: Статус + цена + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 100, + alignment: Alignment.center, + padding: EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.3), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + fine.isPaid ? 'ОПЛАЧЕН' : 'НЕ ОПЛАЧЕН', + style: TextStyle( + color: fine.isPaid + ? Colors.greenAccent + : Colors.redAccent, + fontSize: 12, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ), + const SizedBox(height: 8), + Text( + price, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + ), + ), + ); + } + + String _formatDateFromFineDate(DateTime dateTime) { + final now = DateTime.now(); + final localDate = dateTime.toLocal(); + + final isToday = localDate.year == now.year && + localDate.month == now.month && + localDate.day == now.day; + + final yesterday = now.subtract(const Duration(days: 1)); + final isYesterday = localDate.year == yesterday.year && + localDate.month == yesterday.month && + localDate.day == yesterday.day; + + final time = '${localDate.hour.toString().padLeft(2, '0')}:${localDate.minute.toString().padLeft(2, '0')}'; + + if (isToday) { + return 'Сегодня, $time'; + } else if (isYesterday) { + return 'Вчера, $time'; + } else { + const months = [ + 'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', + 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря' + ]; + return '${localDate.day} ${months[localDate.month - 1]}, $time'; + } + } + + String _formatDateFull(DateTime date) { + const months = [ + 'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', + 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря' + ]; + return '${date.day} ${months[date.month - 1]}'; + } +} \ No newline at end of file diff --git a/lib/presentation/screens/fine_payment_screen.dart b/lib/presentation/screens/fine_payment_screen.dart index 4cf3a80..9709924 100644 --- a/lib/presentation/screens/fine_payment_screen.dart +++ b/lib/presentation/screens/fine_payment_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import '../../core/app_colors.dart'; +import '../../data/network/api_service.dart'; import '../../di/service_locator.dart'; import '../../domain/entities/payment_card.dart'; import '../../domain/usecase/add_payment_card_usecase.dart'; @@ -34,13 +35,20 @@ class FinePaymentScreen extends StatelessWidget { getFineDetailsUsecase: getIt(), payFineUsecase: getIt(), )..add(FetchFineDetails(fineId)), - child: const _FinePaymentScreenContent(), + child: _FinePaymentScreenContent(fineId: fineId), ); } } class _FinePaymentScreenContent extends StatelessWidget { - const _FinePaymentScreenContent(); + final int fineId; + + const _FinePaymentScreenContent({required this.fineId}); + + String _resolveImageUrl(String path) { + if (path.startsWith('http://') || path.startsWith('https://')) return path; + return 'https://sharing-api.sparkit.by/$path'; + } void _openFullScreenImage(BuildContext context, List photos, int initialIndex) { Navigator.of(context).push( @@ -88,24 +96,30 @@ class _FinePaymentScreenContent extends StatelessWidget { listenWhen: (previous, current) => previous.status != current.status, listener: (context, state) { if (state.status == FinePaymentStatus.success && state.paymentCompleted) { - context.go('/home'); + context.go('/home/fine-payment/$fineId/result/success'); } else if (state.status == FinePaymentStatus.failure) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - state.errorMessage ?? 'Произошла ошибка при оплате штрафа', - style: const TextStyle(color: Colors.white), + if (state.fine != null) { + // штраф уже был загружен — значит ошибка произошла при попытке оплаты + context.go('/home/fine-payment/$fineId/result/failed'); + } else { + // штраф не удалось загрузить — остаёмся на месте, показываем снекбар + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + state.errorMessage ?? 'Произошла ошибка при оплате штрафа', + style: const TextStyle(color: Colors.white), + ), + backgroundColor: Colors.redAccent.withOpacity(0.9), + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + margin: const EdgeInsets.all(20), + duration: const Duration(seconds: 3), ), - backgroundColor: Colors.redAccent.withOpacity(0.9), - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - margin: const EdgeInsets.all(20), - duration: const Duration(seconds: 3), - ), - ); + ); + } } }, builder: (context, state) { @@ -122,6 +136,8 @@ class _FinePaymentScreenContent extends StatelessWidget { if (fine == null) return const SizedBox.shrink(); + final bool isPaid = fine.isPaid; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -136,7 +152,7 @@ class _FinePaymentScreenContent extends StatelessWidget { separatorBuilder: (_, __) => const SizedBox(width: 12), itemBuilder: (context, index) { final photo = fine.photoUrls[index]; - final isNetwork = photo.startsWith('http'); + final imageUrl = _resolveImageUrl(photo); return GestureDetector( onTap: () => _openFullScreenImage( context, @@ -145,18 +161,35 @@ class _FinePaymentScreenContent extends StatelessWidget { ), child: ClipRRect( borderRadius: BorderRadius.circular(16), - child: isNetwork - ? Image.network( - photo, - width: 110, - height: 110, - fit: BoxFit.cover, - ) - : Image.asset( - photo, + child: Image.network( + imageUrl, width: 110, height: 110, fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return SizedBox( + width: 110, + height: 110, + child: Center( + child: SizedBox( + width: 28, + height: 28, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ), + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + return const Icon(Icons.error, color: Colors.white, size: 48); + }, ), ), ); @@ -166,17 +199,17 @@ class _FinePaymentScreenContent extends StatelessWidget { const SizedBox(height: 24), - _buildInfoLabel('Самокат:'), + _buildInfoLabel('Номер заказа:'), _buildValueRow( 'qr_icon.png', - fine.scooterNumber, + '${fine.orderId}', ), const SizedBox(height: 20), - _buildInfoLabel('Дата:'), + _buildInfoLabel('Дата и время:'), Text( - fine.date, + _formatDateTime(fine.createdAtDateTime), style: const TextStyle( color: Colors.white, fontSize: 18, @@ -244,111 +277,112 @@ class _FinePaymentScreenContent extends StatelessWidget { const SizedBox(height: 28), - (state.useBalance || state.selectedCard != null) - ? PaymentOption( - title: state.useBalance ? 'Баланс' : "", - subtitle: state.useBalance - ? '${state.userBalance.toStringAsFixed(2)} BYN' - : '${state.selectedCard!.cardLastNumber}', - isSelected: true, - onTap: () async { - final result = await showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (innerContext) => BlocProvider( - create: (context) => PaymentMethodSheetBloc( - getIt(), - getIt(), - getIt(), - getIt(), - )..add(PaymentMethodSheetStarted()), - child: PaymentMethodSheet( - initialSelectedCard: state.useBalance ? null : state.selectedCard, - ), - ), - ); - - if (result != null && context.mounted) { - if (result is PaymentCard) { - context.read().add(FinePaymentCardChanged(result)); - } else if (result == 'balance') { - context.read().add(SelectFineBalancePressed()); - } - } - }, - ) - : Container( - height: 56, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - border: Border.all( - color: Colors.white.withOpacity(0.4), - width: 1, - ), - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () async { - final result = await showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (innerContext) => BlocProvider( - create: (context) => PaymentMethodSheetBloc( - getIt(), - getIt(), - getIt(), - getIt(), - )..add(PaymentMethodSheetStarted()), - child: const PaymentMethodSheet(), + if (!isPaid) + (state.useBalance || state.selectedCard != null) + ? PaymentOption( + title: state.useBalance ? 'Баланс' : "", + subtitle: state.useBalance + ? '${state.userBalance.toStringAsFixed(2)} BYN' + : '${state.selectedCard!.cardLastNumber}', + isSelected: true, + onTap: () async { + final result = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (innerContext) => BlocProvider( + create: (context) => PaymentMethodSheetBloc( + getIt(), + getIt(), + getIt(), + getIt(), + )..add(PaymentMethodSheetStarted()), + child: PaymentMethodSheet( + initialSelectedCard: state.useBalance ? null : state.selectedCard, ), - ); + ), + ); - if (result != null && context.mounted) { - if (result is PaymentCard) { - context.read().add(FinePaymentCardChanged(result)); - } else if (result == 'balance') { - context.read().add(SelectFineBalancePressed()); - } + if (result != null && context.mounted) { + if (result is PaymentCard) { + context.read().add(FinePaymentCardChanged(result)); + } else if (result == 'balance') { + context.read().add(SelectFineBalancePressed()); } - }, + } + }, + ) + : Container( + height: 56, + decoration: BoxDecoration( borderRadius: BorderRadius.circular(24), - child: Center( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'Способ оплаты', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, + border: Border.all( + color: Colors.white.withOpacity(0.4), + width: 1, + ), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () async { + final result = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (innerContext) => BlocProvider( + create: (context) => PaymentMethodSheetBloc( + getIt(), + getIt(), + getIt(), + getIt(), + )..add(PaymentMethodSheetStarted()), + child: const PaymentMethodSheet(), + ), + ); + + if (result != null && context.mounted) { + if (result is PaymentCard) { + context.read().add(FinePaymentCardChanged(result)); + } else if (result == 'balance') { + context.read().add(SelectFineBalancePressed()); + } + } + }, + borderRadius: BorderRadius.circular(24), + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Способ оплаты', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), ), - ), - const SizedBox(width: 8), - Icon( - Icons.arrow_forward_ios, - size: 12, - color: Colors.white.withOpacity(0.6), - ), - Icon( - Icons.arrow_forward_ios, - size: 12, - color: Colors.white.withOpacity(0.4), - ), - Icon( - Icons.arrow_forward_ios, - size: 12, - color: Colors.white.withOpacity(0.2), - ), - ], + const SizedBox(width: 8), + Icon( + Icons.arrow_forward_ios, + size: 12, + color: Colors.white.withOpacity(0.6), + ), + Icon( + Icons.arrow_forward_ios, + size: 12, + color: Colors.white.withOpacity(0.4), + ), + Icon( + Icons.arrow_forward_ios, + size: 12, + color: Colors.white.withOpacity(0.2), + ), + ], + ), ), ), ), ), - ), const SizedBox(height: 20), ], ); @@ -361,6 +395,11 @@ class _FinePaymentScreenContent extends StatelessWidget { padding: const EdgeInsets.all(24.0), child: BlocBuilder( builder: (context, state) { + final fine = state.fine; + if (fine == null || fine.isPaid) { + return const SizedBox.shrink(); + } + final isButtonDisabled = state.status == FinePaymentStatus.loading || (!state.useBalance && state.selectedCard == null); @@ -418,4 +457,32 @@ class _FinePaymentScreenContent extends StatelessWidget { ], ); } -} \ No newline at end of file + + String _formatDateTime(DateTime dateTime) { + final now = DateTime.now(); + final localDate = dateTime.toLocal(); + + final isToday = localDate.year == now.year && + localDate.month == now.month && + localDate.day == now.day; + + final yesterday = now.subtract(const Duration(days: 1)); + final isYesterday = localDate.year == yesterday.year && + localDate.month == yesterday.month && + localDate.day == yesterday.day; + + final time = '${localDate.hour.toString().padLeft(2, '0')}:${localDate.minute.toString().padLeft(2, '0')}'; + + if (isToday) { + return 'Сегодня, $time'; + } else if (isYesterday) { + return 'Вчера, $time'; + } else { + const months = [ + 'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', + 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря' + ]; + return '${localDate.day} ${months[localDate.month - 1]}, $time'; + } + } +} diff --git a/lib/presentation/screens/order_history_screen.dart b/lib/presentation/screens/order_history_screen.dart index c1db72e..8558d01 100644 --- a/lib/presentation/screens/order_history_screen.dart +++ b/lib/presentation/screens/order_history_screen.dart @@ -193,7 +193,7 @@ class _EmptyState extends StatelessWidget { const SizedBox(height: 40), const Text( - 'У вас пока нет завершенных поездок.', + 'У вас пока нет завершенных поездок', textAlign: TextAlign.center, style: TextStyle( color: Colors.white, @@ -232,7 +232,7 @@ class _OrderCard extends StatelessWidget { Widget build(BuildContext context) { final date = _formatDate(order.startAt ?? order.finishAt ?? DateTime.now()); final scooterNumber = order.scooter?.title ?? '№${order.scooterId}'; - final price = '${order.totalPrice?.toStringAsFixed(2)} BYN' ?? '${order.totalPrice?.toStringAsFixed(2) ?? '0.00'} BYN'; + final price = '${order.totalPrice?.toStringAsFixed(2)} руб.' ?? '${order.totalPrice?.toStringAsFixed(2) ?? '0.00'} BYN'; return GestureDetector( onTap: () { diff --git a/lib/presentation/screens/phone_screen.dart b/lib/presentation/screens/phone_screen.dart index eee2273..2877960 100644 --- a/lib/presentation/screens/phone_screen.dart +++ b/lib/presentation/screens/phone_screen.dart @@ -22,41 +22,6 @@ class PhoneScreen extends StatefulWidget { class _PhoneScreenState extends State { final TextEditingController _phoneController = TextEditingController(); - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - _showTestModeDialog(); - }); - } - - void _showTestModeDialog() { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return AlertDialog( - title: const Text( - 'Внимание!', - style: TextStyle(fontWeight: FontWeight.bold), - ), - content: const Text('Приложение находится в тестовом режиме!'), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text( - 'Продолжить', - style: TextStyle(color: Colors.blueAccent), - ), - ), - ], - ); - }, - ); - } - @override void dispose() { _phoneController.dispose(); diff --git a/lib/presentation/screens/support_screen.dart b/lib/presentation/screens/support_screen.dart index 3bd6193..967d790 100644 --- a/lib/presentation/screens/support_screen.dart +++ b/lib/presentation/screens/support_screen.dart @@ -26,21 +26,21 @@ class SupportScreen extends StatelessWidget { LinkRow( icon: 'assets/icons/telegram.png', title: 'Telegram', - onTap: () => openLink('https://t.me/...'), + onTap: () => openLink('https://t.me/BeHappySupportt'), ), const Divider(height: 1, color: Colors.white24), const SizedBox(height: 12), LinkRow( icon: 'assets/icons/whatsapp.png', title: 'WhatsApp', - onTap: () => openLink('https://wa.me/...'), + onTap: () => openLink('https://wa.me/375257110909'), ), const Divider(height: 1, color: Colors.white24), const SizedBox(height: 12), LinkRow( icon: 'assets/icons/viber.png', title: 'Viber', - onTap: () => openLink('viber://chat?number=...'), + onTap: () => openLink('viber://chat?number=+375257110909'), ), const Divider(height: 1, color: Colors.white24), const SizedBox(height: 12), diff --git a/lib/presentation/state/fine_list_state.dart b/lib/presentation/state/fine_list_state.dart new file mode 100644 index 0000000..33ee8a3 --- /dev/null +++ b/lib/presentation/state/fine_list_state.dart @@ -0,0 +1,31 @@ +import 'package:equatable/equatable.dart'; +import '../../domain/entities/fine.dart'; + +enum FineListStatus { loading, success, failure, empty } + +class FineListState extends Equatable { + final FineListStatus status; + final List fines; + final String? errorMessage; + + const FineListState({ + this.status = FineListStatus.loading, + this.fines = const [], + this.errorMessage, + }); + + FineListState copyWith({ + FineListStatus? status, + List? fines, + String? errorMessage, + }) { + return FineListState( + status: status ?? this.status, + fines: fines ?? this.fines, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + @override + List get props => [status, fines, errorMessage]; +} \ No newline at end of file diff --git a/lib/presentation/viewmodel/fine_list_bloc.dart b/lib/presentation/viewmodel/fine_list_bloc.dart new file mode 100644 index 0000000..afdc8f0 --- /dev/null +++ b/lib/presentation/viewmodel/fine_list_bloc.dart @@ -0,0 +1,73 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../core/result.dart'; // Убедитесь, что импортирован! +import '../../domain/entities/fine.dart'; +import '../../domain/usecase/get_fines_usecase.dart'; +import '../event/fine_list_event.dart'; +import '../state/fine_list_state.dart'; + +class FineListBloc extends Bloc { + final GetFinesUsecase getFinesUsecase; + + FineListBloc(this.getFinesUsecase) : super(const FineListState()) { + on(_onFetchFines); + on(_onRefreshFines); + } + + Future _onFetchFines( + FetchFinesRequested event, + Emitter emit, + ) async { + emit(state.copyWith(status: FineListStatus.loading)); + try { + final result = await getFinesUsecase(); // Возвращает Result> + + // Распаковываем Result + if (result is Success>) { + final fines = result.data ?? []; // List + emit(state.copyWith( + status: fines.isEmpty ? FineListStatus.empty : FineListStatus.success, + fines: fines, + )); + } else if (result is Failure>) { + emit(state.copyWith( + status: FineListStatus.failure, + errorMessage: result.failure.toString(), // или конкретное сообщение + )); + } + } catch (e) { + emit(state.copyWith( + status: FineListStatus.failure, + errorMessage: e.toString(), + )); + } + } + + Future _onRefreshFines( + RefreshFinesRequested event, + Emitter emit, + ) async { + emit(state.copyWith(status: FineListStatus.loading)); + try { + final result = await getFinesUsecase(); // Возвращает Result> + + // Распаковываем Result + if (result is Success>) { + final fines = result.data ?? []; // List + emit(state.copyWith( + status: fines.isEmpty ? FineListStatus.empty : FineListStatus.success, + fines: fines, + )); + } else if (result is Failure>) { + emit(state.copyWith( + status: FineListStatus.failure, + errorMessage: result.failure.toString(), // или конкретное сообщение + )); + } + } catch (e) { + emit(state.copyWith( + status: FineListStatus.failure, + errorMessage: e.toString(), + )); + } + } +} \ No newline at end of file diff --git a/lib/presentation/viewmodel/fine_payment_bloc.dart b/lib/presentation/viewmodel/fine_payment_bloc.dart index 0b6221f..4b6706d 100644 --- a/lib/presentation/viewmodel/fine_payment_bloc.dart +++ b/lib/presentation/viewmodel/fine_payment_bloc.dart @@ -1,4 +1,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../core/result.dart';import '../../domain/entities/fine.dart'; + import '../../domain/usecase/get_fine_details_usecase.dart'; import '../../domain/usecase/pay_fine_usecase.dart'; import '../event/fine_payment_event.dart'; @@ -19,13 +21,37 @@ class FinePaymentBloc extends Bloc { } Future _onFetchFineDetails( - FetchFineDetails event, - Emitter emit, - ) async { + FetchFineDetails event, + Emitter emit, + ) async { emit(state.copyWith(status: FinePaymentStatus.loading)); try { - final fine = await getFineDetailsUsecase(event.fineId); - // emit(state.copyWith(status: FinePaymentStatus.initial, fine: fine)); + final result = await getFineDetailsUsecase(event.fineId); // Возвращает Result + + // Распаковываем Result + if (result is Success) { + final fine = result.data; // Fine? + if (fine != null) { + emit(state.copyWith( + status: FinePaymentStatus.initial, + fine: fine, + )); + } else { + emit( + state.copyWith( + status: FinePaymentStatus.failure, + errorMessage: "Не удалось получить данные о штрафе", + ), + ); + } + } else if (result is Failure) { + emit( + state.copyWith( + status: FinePaymentStatus.failure, + errorMessage: result.failure.toString(), // или конкретное сообщение + ), + ); + } } catch (e) { emit( state.copyWith( diff --git a/pubspec.yaml b/pubspec.yaml index 9c696a4..a1dd423 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: be_happy description: "" publish_to: 'none' -version: 1.0.0+21 +version: 1.0.0+22 environment: sdk: ^3.8.1