diff --git a/assets/animations/lottie/warningAnimation.json b/assets/animations/lottie/warningAnimation.json
new file mode 100644
index 0000000..43dace1
--- /dev/null
+++ b/assets/animations/lottie/warningAnimation.json
@@ -0,0 +1 @@
+{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.1.1","a":"Bilal Arief","k":"warning, pending, icon, animation, web, mobile, app, yellow, simple, indicator, caution, alertness, attention, notice, cautionary, warning icon, pending icon.","d":"Conveying caution and alertness, this simple yet eye-catching yellow animation icon is the perfect addition to any web or mobile application as a warning or pending indicator.","tc":"Yellow"},"fr":60,"ip":0,"op":104,"w":1080,"h":1080,"nm":"Warning","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"White-CircleExclamationMark","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":52,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.964],"y":[0]},"t":53,"s":[3]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.89],"y":[0]},"t":60,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.759],"y":[0]},"t":65,"s":[-3]},{"t":67,"s":[0]}],"ix":10},"p":{"a":0,"k":[540.306,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.779,0.779,0.333],"y":[0,0,0]},"t":47,"s":[100,100,100]},{"i":{"x":[0.216,0.216,0.667],"y":[1,1,1]},"o":{"x":[0.597,0.597,0.333],"y":[0,0,0]},"t":56,"s":[115,115,100]},{"t":67,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50,50],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,175],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.19],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[100]},{"t":77,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"gr","it":[{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.19],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[100]},{"t":77,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":1,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":666,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"White-BodyExclamationMark","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":52,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.964],"y":[0]},"t":53,"s":[3]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.89],"y":[0]},"t":60,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.759],"y":[0]},"t":65,"s":[-3]},{"t":67,"s":[0]}],"ix":10},"p":{"a":0,"k":[539.694,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.779,0.779,0.333],"y":[0,0,0]},"t":47,"s":[87,87,100]},{"i":{"x":[0.216,0.216,0.667],"y":[1,1,1]},"o":{"x":[0.597,0.597,0.333],"y":[0,0,0]},"t":56,"s":[115,115,100]},{"t":67,"s":[87,87,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[51.5,-203.25],[-49.5,-203.25],[1,129.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":55,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[70.455,95.556],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.19],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[0]},{"t":77,"s":[100]}],"ix":1},"e":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"gr","it":[{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":1,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.19],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[0]},{"t":77,"s":[100]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":1,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":666,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"LightYellow-CircleExclamationMark","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540.306,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50,50],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.803921568627,0.301960784314,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,175],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.19],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":9,"s":[0]},{"t":67,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.221],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":9,"s":[0]},{"t":31,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":666,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"DarkYellow-CircleExclamationMark","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540.306,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50,50],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.929411764706,0.666666666667,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,175],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.19],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":16,"s":[0]},{"t":74,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.221],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":16,"s":[0]},{"t":38,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":666,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"LightYellow-BodyExclamationMark","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[539.694,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[87,87,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[51.5,-203.25],[-49.5,-203.25],[1,129.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.803921568627,0.301960784314,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":55,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[70.455,95.556],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.19],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":9,"s":[0]},{"t":67,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.221],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":9,"s":[0]},{"t":31,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":666,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"DarkYellow-BodyExclamationMark","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[539.694,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[87,87,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[51.5,-203.25],[-49.5,-203.25],[1,129.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.929411764706,0.666666666667,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":55,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[70.455,95.556],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.19],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":16,"s":[0]},{"t":74,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.221],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":16,"s":[0]},{"t":38,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":666,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"DarkYellow-CircleStroke","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[539.013,535.349,0],"ix":2},"a":{"a":0,"k":[27.013,127.349,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[784,784],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.057],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":17,"s":[100]},{"t":47,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.329],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":17,"s":[100]},{"t":75,"s":[0]}],"ix":2},"o":{"a":0,"k":9000,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.929411764706,0.666989614449,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":35,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[28,132],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":666,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"LightYellow-CircleStroke","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[539.013,535.349,0],"ix":2},"a":{"a":0,"k":[27.013,127.349,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[784,784],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.057],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":41,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.329],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":71,"s":[0]}],"ix":2},"o":{"a":0,"k":9000,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.802906589882,0.301960754395,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":35,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[28,132],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":666,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Yellow-Circle","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":28,"s":[25]},{"t":39,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[544.794,562.601,0],"ix":2},"a":{"a":0,"k":[32.429,152.879,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.354,0.354,0.667],"y":[1.438,1.438,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":13,"s":[0,0,100]},{"t":37,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[784,784],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.717647058824,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[28,132],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":666,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/assets/images/svg/doctor_contact_icon.svg b/assets/images/svg/doctor_contact_icon.svg
new file mode 100644
index 0000000..00cba33
--- /dev/null
+++ b/assets/images/svg/doctor_contact_icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/images/svg/doctor_profile_icon.svg b/assets/images/svg/doctor_profile_icon.svg
new file mode 100644
index 0000000..120c3c1
--- /dev/null
+++ b/assets/images/svg/doctor_profile_icon.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/images/svg/free_med_delivery_icon.svg b/assets/images/svg/free_med_delivery_icon.svg
new file mode 100644
index 0000000..626e035
--- /dev/null
+++ b/assets/images/svg/free_med_delivery_icon.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/images/svg/immediate_service_icon.svg b/assets/images/svg/immediate_service_icon.svg
new file mode 100644
index 0000000..261ac9a
--- /dev/null
+++ b/assets/images/svg/immediate_service_icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/images/svg/livecare_book_icon.svg b/assets/images/svg/livecare_book_icon.svg
new file mode 100644
index 0000000..3d38ac7
--- /dev/null
+++ b/assets/images/svg/livecare_book_icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/svg/livecare_clinic_icon.svg b/assets/images/svg/livecare_clinic_icon.svg
new file mode 100644
index 0000000..2b60209
--- /dev/null
+++ b/assets/images/svg/livecare_clinic_icon.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/images/svg/no_visit_icon.svg b/assets/images/svg/no_visit_icon.svg
new file mode 100644
index 0000000..2c8616a
--- /dev/null
+++ b/assets/images/svg/no_visit_icon.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json
index e054238..df0f391 100644
--- a/assets/langs/en-US.json
+++ b/assets/langs/en-US.json
@@ -754,7 +754,7 @@
"thisItemIsNotAvailable": "This item is not available",
"beforeAfterImages": "Before & After Images",
"clinicAcceptLivecare": "No need to wait or visit You can now get medical consultation via Video call (LiveCare service) in The name of the clinic clinic and the doctor will contact you immediately",
- "livecareModalTop": "This Clinic is Accepting livecare services",
+ "livecareModalTop": "This Clinic is Accepting LiveCare services",
"livecarePoint2": "A Specialized doctor will contact you",
"livecarePoint3": "A Doctor will be able to see your full medical file history",
"livecarePoint4": "Free Medicine Delivery Available",
@@ -811,4 +811,5 @@
"pendingActivation": "Pending Activation",
"awaitingApproval": "Awaiting Approval",
"ready": "Ready"
+
}
\ No newline at end of file
diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart
index 9518fd9..4aef9ec 100644
--- a/lib/core/app_assets.dart
+++ b/lib/core/app_assets.dart
@@ -108,6 +108,13 @@ class AppAssets {
static const String search_by_clinic_icon = '$svgBasePath/search_by_clinic_icon.svg';
static const String search_by_doctor_icon = '$svgBasePath/search_by_doctor_icon.svg';
static const String search_by_region_icon = '$svgBasePath/search_by_region_icon.svg';
+ static const String livecare_clinic_icon = '$svgBasePath/livecare_clinic_icon.svg';
+ static const String immediate_service_icon = '$svgBasePath/immediate_service_icon.svg';
+ static const String no_visit_icon = '$svgBasePath/no_visit_icon.svg';
+ static const String doctor_contact_icon = '$svgBasePath/doctor_contact_icon.svg';
+ static const String free_med_delivery_icon = '$svgBasePath/free_med_delivery_icon.svg';
+ static const String livecare_book_icon = '$svgBasePath/livecare_book_icon.svg';
+ static const String doctor_profile_icon = '$svgBasePath/doctor_profile_icon.svg';
//bottom navigation//
static const String homeBottom = '$svgBasePath/home_bottom.svg';
@@ -137,4 +144,5 @@ class AppAnimations {
static const String checkmark = '$lottieBasePath/checkmark.json';
static const String loadingAnimation = '$lottieBasePath/Loader.json';
static const String errorAnimation = '$lottieBasePath/ErrorAnimation.json';
+ static const String warningAnimation = '$lottieBasePath/warningAnimation.json';
}
diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart
index 6edd3cc..90e3702 100644
--- a/lib/core/dependencies.dart
+++ b/lib/core/dependencies.dart
@@ -145,7 +145,6 @@ class AppDependencies {
),
);
-
getIt.registerLazySingleton(
() => MedicalFileViewModel(
medicalFileRepo: getIt(),
@@ -157,6 +156,8 @@ class AppDependencies {
() => BookAppointmentsViewModel(
bookAppointmentsRepo: getIt(),
errorHandlerService: getIt(),
+ navigationService: getIt(),
+ myAppointmentsViewModel: getIt()
),
);
diff --git a/lib/core/utils/loading_utils.dart b/lib/core/utils/loading_utils.dart
index 16555e8..1a99278 100644
--- a/lib/core/utils/loading_utils.dart
+++ b/lib/core/utils/loading_utils.dart
@@ -2,6 +2,9 @@ import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
+import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
+import 'package:hmg_patient_app_new/core/utils/utils.dart';
+import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
@@ -12,7 +15,7 @@ class LoadingUtils {
static bool get isLoading => _isLoadingVisible;
- static showFullScreenLoader({bool barrierDismissible = true}) {
+ static showFullScreenLoader({bool barrierDismissible = true, isSuccessDialog = false, String loadingText = "Loading, Please wait..."}) {
if (!_isLoadingVisible) {
_isLoadingVisible = true;
final context = _navigationService.navigatorKey.currentContext;
@@ -20,15 +23,25 @@ class LoadingUtils {
if (context == null) return;
showDialog(
- barrierDismissible: barrierDismissible,
- context: context,
- barrierColor: AppColors.blackColor.withOpacity(0.5),
- useRootNavigator: false,
- builder: (BuildContext context) => Center(
- child: CircularProgressIndicator(
- color: AppColors.primaryRedColor,
- )),
- ).then((value) {
+ barrierDismissible: barrierDismissible,
+ context: context,
+ barrierColor: AppColors.blackColor.withOpacity(0.5),
+ useRootNavigator: false,
+ useSafeArea: false,
+ builder: (BuildContext context) {
+ return Container(
+ decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
+ color: AppColors.whiteColor,
+ borderRadius: 20.h,
+ hasShadow: false,
+ ),
+ child: Material(
+ child: Center(
+ child: isSuccessDialog ? Utils.getSuccessWidget(loadingText: loadingText) : Utils.getLoadingWidget(loadingText: loadingText),
+ ),
+ ),
+ );
+ }).then((value) {
_isLoadingVisible = false;
});
}
diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart
index 09f455b..d34a44c 100644
--- a/lib/core/utils/utils.dart
+++ b/lib/core/utils/utils.dart
@@ -20,6 +20,7 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
+import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/dialogs/confirm_dialog.dart';
import 'package:hmg_patient_app_new/widgets/loading_dialog.dart';
import 'package:lottie/lottie.dart';
@@ -340,6 +341,55 @@ class Utils {
).center;
}
+ static Widget getWarningWidget({String? loadingText, bool isShowActionButtons = false, Function? onConfirmTap, Function? onCancelTap}) {
+ return Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Lottie.asset(AppAnimations.warningAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
+ SizedBox(height: 8.h),
+ (loadingText ?? LocaleKeys.loadingText.tr()).toText14(color: AppColors.blackColor),
+ SizedBox(height: 16.h),
+ isShowActionButtons
+ ? Row(
+ children: [
+ Expanded(
+ child: CustomButton(
+ text: LocaleKeys.cancel.tr(),
+ onPressed: () {
+ if (onCancelTap != null) {
+ onCancelTap();
+ }
+ },
+ backgroundColor: AppColors.secondaryLightRedColor,
+ borderColor: AppColors.secondaryLightRedColor,
+ textColor: AppColors.primaryRedColor,
+ icon: AppAssets.cancel,
+ iconColor: AppColors.primaryRedColor,
+ ),
+ ),
+ SizedBox(width: 8.h),
+ Expanded(
+ child: CustomButton(
+ text: LocaleKeys.confirm.tr(),
+ onPressed: () async {
+ if (onConfirmTap != null) {
+ onConfirmTap();
+ }
+ },
+ backgroundColor: AppColors.bgGreenColor,
+ borderColor: AppColors.bgGreenColor,
+ textColor: Colors.white,
+ icon: AppAssets.confirm,
+ ),
+ ),
+ ],
+ )
+ : SizedBox.shrink(),
+ ],
+ ).center;
+ }
+
static bool isVidaPlusProject(int projectID) {
AppState appState = getIt.get();
bool isVidaPlus = false;
diff --git a/lib/features/authentication/widgets/otp_verification_screen.dart b/lib/features/authentication/widgets/otp_verification_screen.dart
index 1ea9be5..abb36a0 100644
--- a/lib/features/authentication/widgets/otp_verification_screen.dart
+++ b/lib/features/authentication/widgets/otp_verification_screen.dart
@@ -438,12 +438,13 @@ class _OTPVerificationScreenState extends State {
void _onOtpChanged(String value) {
// Handle clipboard paste or programmatic input
- if (value.length > 1) {
- String? otp = _extractOtpFromText(value);
- if (otp != null) {
- autoFillOtp(otp);
- return;
- }
+ if (value.length >= 4) {
+ _onOtpCompleted(value);
+ // String? otp = _extractOtpFromText(value);
+ // if (otp != null) {
+ // autoFillOtp(otp);
+ // return;
+ // }
}
// The OTPWidget will automatically call onDone when complete
@@ -622,19 +623,53 @@ class _OTPVerificationScreenState extends State {
/// Auto fill OTP into text fields
void autoFillOtp(String otp) {
+ if (!mounted) return;
if (otp.length != _otpLength) return;
- // Clear any existing text first
- _otpController.clear();
-
- // Add a small delay to ensure the UI is updated
- Future.delayed(const Duration(milliseconds: 50), () {
- _otpController.text = otp;
- // Move cursor to the end
- _otpController.selection = TextSelection.fromPosition(
- TextPosition(offset: otp.length),
- );
- });
+ try {
+ // Clear any existing text first
+ _otpController.clear();
+
+ // Use WidgetsBinding to ensure the widget tree is ready
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (!mounted) return;
+
+ try {
+ // Set the text first
+ _otpController.text = otp;
+
+ // Use a longer delay for iOS and add validation
+ Future.delayed(const Duration(milliseconds: 300), () {
+ if (!mounted) return;
+
+ try {
+ // Only attempt to set selection if conditions are met
+ if (_otpController.text == otp &&
+ _otpController.text.length == _otpLength &&
+ _otpController.text.length <= _otpController.text.length) {
+
+ final newSelection = TextSelection.fromPosition(
+ TextPosition(offset: _otpController.text.length),
+ );
+
+ // Validate selection before setting
+ if (newSelection.baseOffset <= _otpController.text.length &&
+ newSelection.extentOffset <= _otpController.text.length) {
+ _otpController.selection = newSelection;
+ }
+ }
+ } catch (selectionError) {
+ // Silently fail on selection - text is already set correctly
+ debugPrint('Selection error (non-critical): $selectionError');
+ }
+ });
+ } catch (textError) {
+ debugPrint('Error setting OTP text: $textError');
+ }
+ });
+ } catch (e) {
+ debugPrint('Error in autoFillOtp: $e');
+ }
}
/// Clear OTP fields
diff --git a/lib/features/book_appointments/book_appointments_repo.dart b/lib/features/book_appointments/book_appointments_repo.dart
index 5b0bf10..5a93879 100644
--- a/lib/features/book_appointments/book_appointments_repo.dart
+++ b/lib/features/book_appointments/book_appointments_repo.dart
@@ -1,10 +1,45 @@
import 'package:dartz/dartz.dart';
import 'package:hmg_patient_app_new/core/api/api_client.dart';
+import 'package:hmg_patient_app_new/core/api_consts.dart';
+import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart';
import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart';
+import 'package:hmg_patient_app_new/core/utils/date_util.dart';
+import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctor_profile_response_model.dart';
+import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart';
+import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart';
+import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
abstract class BookAppointmentsRepo {
- Future> getDoctors();
+ Future>>> getClinics();
+
+ Future> getProjects();
+
+ Future>>> getDoctorsList(int clinicID, int projectID, bool isNearest, int doctorId, String doctorName, {isContinueDentalPlan = false});
+
+ Future>> getDoctorProfile(int clinicID, int projectID, int doctorId, {Function(dynamic)? onSuccess, Function(String)? onError});
+
+ Future>> getDoctorFreeSlots(int clinicID, int projectID, int doctorId, bool isBookingForLiveCare, {Function(dynamic)? onSuccess, Function(String)? onError});
+
+ Future>> cancelAppointment({required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel});
+
+ Future>> insertSpecificAppointment(
+ {required int docID,
+ required int clinicID,
+ required int projectID,
+ required String selectedTime,
+ required String selectedDate,
+ required int initialSlotDuration,
+ required int genderID,
+ required int userAge,
+ String? procedureID,
+ num? testTypeEnum,
+ num? testProcedureEnum,
+ int? invoiceNumber,
+ int? lineItemNo,
+ String? invoiceNoVP,
+ Function(dynamic)? onSuccess,
+ Function(String)? onError});
}
class BookAppointmentsRepoImp implements BookAppointmentsRepo {
@@ -14,35 +49,317 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo {
BookAppointmentsRepoImp({required this.loggerService, required this.apiClient});
@override
- Future> getDoctors() async {
+ Future>>> getClinics() async {
+ Map mapDevice = {};
+
try {
- // Mock API call with delayed response
- final result = await Future.delayed(
- const Duration(seconds: 2),
- () => {
- 'success': true,
- 'data': [
- {
- 'id': '1',
- 'name': 'Dr. Ahmed Hassan',
- 'specialty': 'Cardiology',
- 'experience': '10 years',
- 'rating': 4.8,
- 'image': 'https://example.com/doctor1.jpg'
- },
- ]
- }
+ GenericApiModel>? apiResponse;
+ Failure? failure;
+ await apiClient.post(
+ GET_CLINICS_LIST_URL,
+ body: mapDevice,
+ onFailure: (error, statusCode, {messageStatus, failureType}) {
+ failure = failureType;
+ },
+ onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
+ try {
+ final list = response['ListClinicCentralized'];
+ // if (list == null || list.isEmpty) {
+ // throw Exception("lab list is empty");
+ // }
+
+ final clinicsList = list.map((item) => GetClinicsListResponseModel.fromJson(item as Map)).toList().cast();
+
+ apiResponse = GenericApiModel>(
+ messageStatus: messageStatus,
+ statusCode: statusCode,
+ errorMessage: null,
+ data: clinicsList,
+ );
+ } catch (e) {
+ failure = DataParsingFailure(e.toString());
+ }
+ },
);
+ if (failure != null) return Left(failure!);
+ if (apiResponse == null) return Left(ServerFailure("Unknown error"));
+ return Right(apiResponse!);
+ } catch (e) {
+ return Left(UnknownFailure(e.toString()));
+ }
+ }
+
+ @override
+ Future> getProjects() {
+ // TODO: implement getProjects
+ throw UnimplementedError();
+ }
+
+ @override
+ Future>>> getDoctorsList(int clinicID, int projectID, bool isNearest, int doctorId, String doctorName,
+ {isContinueDentalPlan = false, Function(dynamic)? onSuccess, Function(String)? onError}) async {
+ Map mapDevice = {
+ "ClinicID": (doctorName == "") ? clinicID : 0,
+ "ProjectID": projectID,
+ "DoctorName": doctorName, //!= null ? doctorId : 0,
+ "ContinueDentalPlan": isContinueDentalPlan,
+ "IsSearchAppointmnetByClinicID": isContinueDentalPlan ? false : true,
+ "isDentalAllowedBackend": clinicID == 17 ? true : isContinueDentalPlan,
+ "IsGetNearAppointment": isNearest,
+ if (isNearest) "SelectedDate": DateUtil.convertDateToString(DateTime.now()),
+ "License": true
+ };
+
+ try {
+ GenericApiModel>? apiResponse;
+ Failure? failure;
+ await apiClient.post(
+ GET_DOCTORS_LIST_URL,
+ body: mapDevice,
+ onFailure: (error, statusCode, {messageStatus, failureType}) {
+ failure = failureType;
+ onError!(error);
+ },
+ onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
+ try {
+ final list = response['DoctorList'];
+ // if (list == null || list.isEmpty) {
+ // throw Exception("lab list is empty");
+ // }
+
+ final doctorsList = list.map((item) => DoctorsListResponseModel.fromJson(item as Map)).toList().cast();
+
+ apiResponse = GenericApiModel>(
+ messageStatus: messageStatus,
+ statusCode: statusCode,
+ errorMessage: null,
+ data: doctorsList,
+ );
+ } catch (e) {
+ failure = DataParsingFailure(e.toString());
+ }
+ },
+ );
+ if (failure != null) return Left(failure!);
+ if (apiResponse == null) return Left(ServerFailure("Unknown error"));
+ return Right(apiResponse!);
+ } catch (e) {
+ return Left(UnknownFailure(e.toString()));
+ }
+ }
+
+ @override
+ Future>> getDoctorProfile(int clinicID, int projectID, int doctorId, {Function(dynamic)? onSuccess, Function(String)? onError}) async {
+ Map mapDevice = {
+ "ClinicID": clinicID,
+ "ProjectID": projectID,
+ "doctorID": doctorId,
+ "License": true,
+ "IsRegistered": true,
+ };
+
+ try {
+ GenericApiModel? apiResponse;
+ Failure? failure;
+ await apiClient.post(
+ GET_DOCTOR_PROFILE,
+ body: mapDevice,
+ onFailure: (error, statusCode, {messageStatus, failureType}) {
+ failure = failureType;
+ onError!(error);
+ },
+ onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
+ try {
+ final doctorProfileList = response['DoctorProfileList'];
+ if (doctorProfileList == null || doctorProfileList.isEmpty) {
+ // throw Exception("lab list is empty");
+ onError!("Doctor profile not found");
+ }
- if (result != null && result is Map && result['success'] != null && result['success'] != false) {
- return Right(result);
- } else {
- loggerService.errorLogs(result.toString());
- return Left(ServerFailure(result.toString()));
- }
+ final doctorsList = DoctorsProfileResponseModel.fromJson(doctorProfileList[0]);
+
+ apiResponse = GenericApiModel(
+ messageStatus: messageStatus,
+ statusCode: statusCode,
+ errorMessage: null,
+ data: doctorsList,
+ );
+ } catch (e) {
+ failure = DataParsingFailure(e.toString());
+ }
+ },
+ );
+ if (failure != null) return Left(failure!);
+ if (apiResponse == null) return Left(ServerFailure("Unknown error"));
+ return Right(apiResponse!);
+ } catch (e) {
+ return Left(UnknownFailure(e.toString()));
+ }
+ }
+
+ //TODO: Implement the logic for Dental & laser clinics
+ @override
+ Future>> getDoctorFreeSlots(int clinicID, int projectID, int doctorId, bool isBookingForLiveCare,
+ {Function(dynamic)? onSuccess, Function(String)? onError}) async {
+ Map mapDevice = {
+ "DoctorID": doctorId,
+ "IsBookingForLiveCare": isBookingForLiveCare,
+ "ClinicID": clinicID,
+ "ProjectID": projectID,
+ "OriginalClinicID": clinicID,
+ "days": 0,
+ "isReschadual": false,
+ };
+
+ try {
+ GenericApiModel? apiResponse;
+ Failure? failure;
+ await apiClient.post(
+ GET_DOCTOR_FREE_SLOTS,
+ body: mapDevice,
+ onFailure: (error, statusCode, {messageStatus, failureType}) {
+ failure = failureType;
+ },
+ onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
+ try {
+ apiResponse = GenericApiModel(
+ messageStatus: messageStatus,
+ statusCode: statusCode,
+ errorMessage: null,
+ data: response,
+ );
+ } catch (e) {
+ failure = DataParsingFailure(e.toString());
+ }
+ },
+ );
+ if (failure != null) return Left(failure!);
+ if (apiResponse == null) return Left(ServerFailure("Unknown error"));
+ return Right(apiResponse!);
+ } catch (e) {
+ return Left(UnknownFailure(e.toString()));
+ }
+ }
+
+ //TODO: Handle the cases for LiveCare Schedule, Dental & Laser Clinics
+ @override
+ Future> insertSpecificAppointment(
+ {required int docID,
+ required int clinicID,
+ required int projectID,
+ required String selectedTime,
+ required String selectedDate,
+ required int initialSlotDuration,
+ required int genderID,
+ required int userAge,
+ String? procedureID,
+ num? testTypeEnum,
+ num? testProcedureEnum,
+ int? invoiceNumber,
+ int? lineItemNo,
+ String? invoiceNoVP,
+ Function(dynamic p1)? onSuccess,
+ Function(String p1)? onError}) async {
+ // {"AppointmentID":2016072771,"ClinicID":1485,"ProjectID":15,"CancelToReschadual":true,"EndTime":"21:15:00","StartTime":"21:00:00","DoctorID":1485,"IsForLiveCare":false,"OriginalClinicID":0,"OriginalProjectID":0,"StrAppointmentDate":"Tuesday, 16 September 2025","VersionID":19.0,"Channel":3,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"jTarYomiIEKOruN8Z4YINA==","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4770714,"PatientTypeID":1,"PatientType":1,"LanguageID":2,"Latitude":24.698381,"Longitude":46.6804279,"TokenID":"jTarYomiIEKOruN8Z4YINA=="}
+
+ Map mapDevice = {
+ "IsForLiveCare": false,
+ "ProjectID": projectID,
+ "ClinicID": clinicID,
+ "DoctorID": docID,
+ "StartTime": selectedTime,
+ "SelectedTime": selectedTime,
+ "EndTime": selectedTime,
+ "ProcedureID": procedureID,
+ "TestTypeEnum": testTypeEnum,
+ "TestProcedureEnum": testProcedureEnum,
+ "InitialSlotDuration": initialSlotDuration,
+ "StrAppointmentDate": selectedDate,
+ "IsVirtual": false,
+ "BookedBy": 102,
+ "VisitType": 1,
+ "VisitFor": 1,
+ "GenderID": genderID,
+ "Age": userAge,
+ "InvoiceNo": invoiceNumber,
+ "InvoiceNo_VP": invoiceNoVP,
+ "LineItemNo": lineItemNo,
+ };
+
+ try {
+ GenericApiModel? apiResponse;
+ Failure? failure;
+ await apiClient.post(
+ INSERT_SPECIFIC_APPOINTMENT,
+ body: mapDevice,
+ onFailure: (error, statusCode, {messageStatus, failureType}) {
+ failure = failureType;
+ },
+ onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
+ try {
+ final appointmentNo = response['AppointmentNo'];
+
+ apiResponse = GenericApiModel(
+ messageStatus: messageStatus,
+ statusCode: statusCode,
+ errorMessage: response["ErrorEndUserMessage"],
+ data: response,
+ );
+ } catch (e) {
+ failure = DataParsingFailure(e.toString());
+ }
+ },
+ );
+ if (failure != null) return Left(failure!);
+ if (apiResponse == null) return Left(ServerFailure("Unknown error"));
+ return Right(apiResponse!);
+ } catch (e) {
+ return Left(UnknownFailure(e.toString()));
+ }
+ }
+
+ @override
+ Future> cancelAppointment({required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel}) async {
+ Map requestBody = {
+ "AppointmentID": patientAppointmentHistoryResponseModel.appointmentNo,
+ "ClinicID": patientAppointmentHistoryResponseModel.clinicID,
+ "ProjectID": patientAppointmentHistoryResponseModel.projectID,
+ "CancelToReschadual": false,
+ "EndTime": patientAppointmentHistoryResponseModel.endTime,
+ "StartTime": patientAppointmentHistoryResponseModel.startTime,
+ "DoctorID": patientAppointmentHistoryResponseModel.doctorID,
+ "IsForLiveCare": patientAppointmentHistoryResponseModel.isLiveCareAppointment,
+ "OriginalClinicID": patientAppointmentHistoryResponseModel.originalClinicID,
+ "OriginalProjectID": patientAppointmentHistoryResponseModel.originalProjectID
+ };
+
+ try {
+ GenericApiModel? apiResponse;
+ Failure? failure;
+ await apiClient.post(
+ CANCEL_APPOINTMENT,
+ body: requestBody,
+ onFailure: (error, statusCode, {messageStatus, failureType}) {
+ failure = failureType;
+ },
+ onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
+ try {
+ apiResponse = GenericApiModel(
+ messageStatus: messageStatus,
+ statusCode: statusCode,
+ errorMessage: null,
+ data: response,
+ );
+ } catch (e) {
+ failure = DataParsingFailure(e.toString());
+ }
+ },
+ );
+ if (failure != null) return Left(failure!);
+ if (apiResponse == null) return Left(ServerFailure("Unknown error"));
+ return Right(apiResponse!);
} catch (e) {
- loggerService.errorLogs(e.toString());
- return Left(ServerFailure(e.toString()));
+ return Left(UnknownFailure(e.toString()));
}
}
}
diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart
index 8f096dd..854b4fc 100644
--- a/lib/features/book_appointments/book_appointments_view_model.dart
+++ b/lib/features/book_appointments/book_appointments_view_model.dart
@@ -1,17 +1,383 @@
+import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
+import 'package:hmg_patient_app_new/core/app_state.dart';
+import 'package:hmg_patient_app_new/core/dependencies.dart';
+import 'package:hmg_patient_app_new/core/utils/date_util.dart';
+import 'package:hmg_patient_app_new/core/utils/loading_utils.dart';
+import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
+import 'package:hmg_patient_app_new/core/utils/utils.dart';
+import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_repo.dart';
+import 'package:hmg_patient_app_new/features/book_appointments/models/free_slot.dart';
+import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctor_profile_response_model.dart';
+import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart';
+import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart';
+import 'package:hmg_patient_app_new/features/book_appointments/models/timeslots.dart';
+import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart';
+import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart';
+import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
+import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
+import 'package:hmg_patient_app_new/services/navigation_service.dart';
+import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
+import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart';
class BookAppointmentsViewModel extends ChangeNotifier {
int selectedTabIndex = 0;
+ bool isClinicsListLoading = false;
+ bool isDoctorsListLoading = false;
+ bool isDoctorProfileLoading = false;
+ bool isDoctorSearchByNameStarted = false;
+
+ int initialSlotDuration = 0;
+
+ List clinicsList = [];
+ List _filteredClinicsList = [];
+
+ List get filteredClinicsList => _filteredClinicsList;
+
+ List doctorsList = [];
+
+ GetClinicsListResponseModel selectedClinic = GetClinicsListResponseModel();
+ DoctorsListResponseModel selectedDoctor = DoctorsListResponseModel();
+
+ late DoctorsProfileResponseModel doctorsProfileResponseModel;
+
+ List slotsList = [];
+ List docFreeSlots = [];
+ List dayEvents = [];
+ List nextDayEvents = [];
+
+ String selectedAppointmentDate = "";
+ String selectedAppointmentTime = "";
+
+ dynamic freeSlotsResponse;
+
BookAppointmentsRepo bookAppointmentsRepo;
ErrorHandlerService errorHandlerService;
+ final NavigationService navigationService;
+ MyAppointmentsViewModel myAppointmentsViewModel;
+
+ late AppState _appState;
+
+ BookAppointmentsViewModel({required this.bookAppointmentsRepo, required this.errorHandlerService, required this.navigationService, required this.myAppointmentsViewModel});
+
+ void initializeFilteredList() {
+ _filteredClinicsList = List.from(clinicsList);
+ notifyListeners();
+ }
+
+ void filterClinics(String? query) {
+ if (query!.isEmpty) {
+ _filteredClinicsList = List.from(clinicsList);
+ } else {
+ _filteredClinicsList = clinicsList.where((clinic) => clinic.clinicDescription?.toLowerCase().contains(query!.toLowerCase()) ?? false).toList();
+ }
+ notifyListeners();
+ }
+
+ initBookAppointmentViewModel() {
+ _appState = getIt();
+ isClinicsListLoading = true;
+ isDoctorsListLoading = true;
+ isDoctorProfileLoading = true;
+ clinicsList.clear();
+ doctorsList.clear();
+ notifyListeners();
+ }
+
+ setIsDoctorsListLoading(bool value) {
+ if (value) {
+ doctorsList.clear();
+ }
+ isDoctorsListLoading = value;
+ notifyListeners();
+ }
+
+ setIsClinicsListLoading(bool value) {
+ if (value) {
+ clinicsList.clear();
+ }
+ isClinicsListLoading = value;
+ notifyListeners();
+ }
+
+ setSelectedClinic(GetClinicsListResponseModel clinic) {
+ selectedClinic = clinic;
+ notifyListeners();
+ }
+
+ setSelectedDoctor(DoctorsListResponseModel doctor) {
+ selectedDoctor = doctor;
+ notifyListeners();
+ }
- BookAppointmentsViewModel({required this.bookAppointmentsRepo, required this.errorHandlerService});
+ setIsDoctorProfileLoading(bool value) {
+ isDoctorProfileLoading = value;
+ notifyListeners();
+ }
+
+ setIsDoctorSearchByNameStarted(bool value) {
+ isDoctorSearchByNameStarted = value;
+ notifyListeners();
+ }
+
+ setDoctorsProfile(DoctorsProfileResponseModel profile) {
+ doctorsProfileResponseModel = profile;
+ notifyListeners();
+ }
+
+ setSelectedAppointmentDateTime(String date, String time) {
+ selectedAppointmentDate = date;
+ selectedAppointmentTime = time;
+ notifyListeners();
+ }
void onTabChanged(int index) {
selectedTabIndex = index;
notifyListeners();
}
+
+ Future getClinics({Function(dynamic)? onSuccess, Function(String)? onError}) async {
+ final result = await bookAppointmentsRepo.getClinics();
+
+ result.fold(
+ (failure) async => await errorHandlerService.handleError(failure: failure),
+ (apiResponse) {
+ if (apiResponse.messageStatus == 2) {
+ // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
+ } else if (apiResponse.messageStatus == 1) {
+ clinicsList = apiResponse.data!;
+ isClinicsListLoading = false;
+ initializeFilteredList();
+ notifyListeners();
+ if (onSuccess != null) {
+ onSuccess(apiResponse);
+ }
+ }
+ },
+ );
+ }
+
+ //TODO: Make the API dynamic with parameters for ProjectID, isNearest, languageID, doctorId, doctorName
+ Future getDoctorsList(
+ {int projectID = 0, bool isNearest = false, int doctorId = 0, String doctorName = "", isContinueDentalPlan = false, Function(dynamic)? onSuccess, Function(String)? onError}) async {
+ doctorsList.clear();
+ final result = await bookAppointmentsRepo.getDoctorsList(selectedClinic.clinicID ?? 0, projectID, isNearest, doctorId, doctorName);
+
+ result.fold(
+ (failure) async {
+ onError!("No doctors found for the search criteria".needTranslation);
+ },
+ (apiResponse) {
+ if (apiResponse.messageStatus == 2) {
+ // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
+ } else if (apiResponse.messageStatus == 1) {
+ doctorsList = apiResponse.data!;
+ isDoctorsListLoading = false;
+ initializeFilteredList();
+ notifyListeners();
+ if (onSuccess != null) {
+ onSuccess(apiResponse);
+ }
+ }
+ },
+ );
+ }
+
+ Future getDoctorProfile({Function(dynamic)? onSuccess, Function(String)? onError}) async {
+ final result = await bookAppointmentsRepo.getDoctorProfile(selectedDoctor.clinicID ?? 0, selectedDoctor.projectID ?? 0, selectedDoctor.doctorID ?? 0, onError: onError);
+
+ result.fold(
+ (failure) async {},
+ (apiResponse) {
+ if (apiResponse.messageStatus == 2) {
+ onError!(apiResponse.errorMessage ?? "Unknown error occurred");
+ // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
+ } else if (apiResponse.messageStatus == 1) {
+ doctorsProfileResponseModel = apiResponse.data!;
+ notifyListeners();
+ if (onSuccess != null) {
+ onSuccess(apiResponse);
+ }
+ }
+ },
+ );
+ }
+
+ //TODO: Handle the cases for LiveCare Schedule
+ Future getDoctorFreeSlots({bool isBookingForLiveCare = false, Function(dynamic)? onSuccess, Function(String)? onError}) async {
+ docFreeSlots.clear();
+ DateTime date;
+ final DateFormat formatter = DateFormat('HH:mm');
+ final DateFormat dateFormatter = DateFormat('yyyy-MM-dd');
+ Map _eventsParsed;
+
+ final result = await bookAppointmentsRepo.getDoctorFreeSlots(selectedDoctor.clinicID ?? 0, selectedDoctor.projectID ?? 0, selectedDoctor.doctorID ?? 0, isBookingForLiveCare, onError: onError);
+
+ result.fold(
+ (failure) async {
+ print(failure);
+ },
+ (apiResponse) {
+ if (apiResponse.messageStatus == 2) {
+ onError!(apiResponse.errorMessage ?? "Unknown error occurred");
+ // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
+ } else if (apiResponse.messageStatus == 1) {
+ if (apiResponse.data['FreeTimeSlots'] == null || apiResponse.data['FreeTimeSlots'].isEmpty) {
+ onError!("No free slots available".tr());
+ return;
+ }
+ initialSlotDuration = apiResponse.data["InitialSlotDuration"];
+ freeSlotsResponse = apiResponse.data['FreeTimeSlots'];
+ freeSlotsResponse.forEach((element) {
+ // date = (isLiveCareSchedule != null && isLiveCareSchedule)
+ // ? DateUtil.convertStringToDate(element)
+ // :
+ date = DateUtil.convertStringToDateSaudiTimezone(element, int.parse(selectedDoctor.projectID.toString()));
+ slotsList.add(FreeSlot(date, ['slot']));
+ docFreeSlots.add(TimeSlot(isoTime: formatter.format(date), start: new DateTime(date.year, date.month, date.day, 0, 0, 0, 0), end: date, vidaDate: element));
+ });
+
+ notifyListeners();
+ if (onSuccess != null) {
+ onSuccess(apiResponse);
+ }
+ }
+ },
+ );
+ }
+
+ Future cancelAppointment({required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, Function(dynamic)? onSuccess, Function(String)? onError}) async {
+ final result = await bookAppointmentsRepo.cancelAppointment(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel);
+
+ result.fold(
+ (failure) async => await errorHandlerService.handleError(failure: failure),
+ (apiResponse) {
+ if (apiResponse.messageStatus == 2) {
+ onError!(apiResponse.errorMessage!);
+ // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
+ } else if (apiResponse.messageStatus == 1) {
+ notifyListeners();
+ if (onSuccess != null) {
+ onSuccess(apiResponse);
+ }
+ }
+ },
+ );
+ }
+
+ //TODO: Handle the cases for LiveCare Schedule, Dental & Laser Clinics
+ Future insertSpecificAppointment(
+ {
+ // required int docID,
+ // required int clinicID,
+ // required int projectID,
+ // required String selectedTime,
+ // required String selectedDate,
+ // required int initialSlotDuration,
+ // required int genderID,
+ // required int userAge,
+ String? procedureID,
+ num? testTypeEnum,
+ num? testProcedureEnum,
+ int? invoiceNumber,
+ int? lineItemNo,
+ String? invoiceNoVP,
+ Function(dynamic p1)? onSuccess,
+ Function(dynamic p1)? onError}) async {
+ _appState = getIt();
+ final result = await bookAppointmentsRepo.insertSpecificAppointment(
+ docID: selectedDoctor.doctorID!,
+ clinicID: selectedDoctor.clinicID!,
+ projectID: selectedDoctor.projectID!,
+ selectedDate: selectedAppointmentDate,
+ selectedTime: selectedAppointmentTime,
+ initialSlotDuration: initialSlotDuration,
+ genderID: _appState.getAuthenticatedUser()!.gender!,
+ userAge: _appState.getAuthenticatedUser()!.age!,
+ onError: onError);
+
+ result.fold(
+ (failure) async {
+ print(failure);
+ },
+ (apiResponse) {
+ if (apiResponse.messageStatus == 2) {
+ // onError!(apiResponse);
+ LoadingUtils.hideFullScreenLoader();
+ showCommonBottomSheetWithoutHeight(
+ title: LocaleKeys.notice.tr(context: navigationService.navigatorKey.currentContext!),
+ navigationService.navigatorKey.currentContext!,
+ child: Utils.getWarningWidget(
+ loadingText: apiResponse.data["ErrorEndUserMessage"],
+ isShowActionButtons: true,
+ onCancelTap: () {
+ navigationService.pop();
+ },
+ onConfirmTap: () async {
+ navigationService.pop();
+ PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel = PatientAppointmentHistoryResponseModel(
+ appointmentNo: apiResponse.data["SameClinicApptList"][0]['AppointmentNo'],
+ clinicID: apiResponse.data["SameClinicApptList"][0]['ClinicID'],
+ projectID: apiResponse.data["SameClinicApptList"][0]['ProjectID'],
+ endDate: apiResponse.data["SameClinicApptList"][0]['EndTime'],
+ startTime: apiResponse.data["SameClinicApptList"][0]['StartTime'],
+ doctorID: apiResponse.data["SameClinicApptList"][0]['DoctorID'],
+ isLiveCareAppointment: apiResponse.data["SameClinicApptList"][0]['IsLiveCareAppointment'],
+ originalClinicID: 0,
+ originalProjectID: 0,
+ appointmentDate: apiResponse.data["SameClinicApptList"][0]['AppointmentDate'],
+ );
+
+ showCommonBottomSheet(navigationService.navigatorKey.currentContext!,
+ child: Utils.getLoadingWidget(loadingText: "Cancelling your previous appointment....".needTranslation),
+ callBackFunc: (str) {},
+ title: "",
+ height: ResponsiveExtension.screenHeight * 0.3,
+ isCloseButtonVisible: false,
+ isDismissible: false,
+ isFullScreen: false);
+ await cancelAppointment(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel).then((val) async {
+ navigationService.pop();
+ Future.delayed(Duration(milliseconds: 50)).then((value) async {});
+ LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: false, loadingText: "Booking your appointment...".needTranslation);
+ await insertSpecificAppointment(
+ onError: (err) {},
+ onSuccess: (apiResp) async {
+ LoadingUtils.hideFullScreenLoader();
+ await Future.delayed(Duration(milliseconds: 50)).then((value) async {
+ LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: true, loadingText: LocaleKeys.appointmentSuccess.tr());
+ await Future.delayed(Duration(milliseconds: 4000)).then((value) {
+ LoadingUtils.hideFullScreenLoader();
+ Navigator.pushAndRemoveUntil(
+ navigationService.navigatorKey.currentContext!,
+ FadePage(
+ page: LandingNavigation(),
+ ),
+ (r) => false);
+ });
+ });
+ });
+ });
+ }),
+ callBackFunc: () {},
+ isFullScreen: false,
+ isCloseButtonVisible: true,
+ );
+ } else if (apiResponse.messageStatus == 1) {
+ if (apiResponse.data == null || apiResponse.data!.isEmpty) {
+ onError!("No free slots available".tr());
+ return;
+ }
+
+ notifyListeners();
+ if (onSuccess != null) {
+ onSuccess(apiResponse);
+ }
+ }
+ },
+ );
+ }
}
diff --git a/lib/features/book_appointments/models/free_slot.dart b/lib/features/book_appointments/models/free_slot.dart
new file mode 100644
index 0000000..4669298
--- /dev/null
+++ b/lib/features/book_appointments/models/free_slot.dart
@@ -0,0 +1,12 @@
+class FreeSlot {
+ List event;
+ DateTime slot;
+
+
+ FreeSlot(this.slot, this.event);
+
+ @override
+ String toString() {
+ return '{ ${this.slot}, ${this.event} }';
+ }
+}
\ No newline at end of file
diff --git a/lib/features/book_appointments/models/resp_models/doctor_profile_response_model.dart b/lib/features/book_appointments/models/resp_models/doctor_profile_response_model.dart
new file mode 100644
index 0000000..944e659
--- /dev/null
+++ b/lib/features/book_appointments/models/resp_models/doctor_profile_response_model.dart
@@ -0,0 +1,196 @@
+class DoctorsProfileResponseModel {
+ int? doctorID;
+ String? doctorName;
+ dynamic doctorNameN;
+ int? clinicID;
+ String? clinicDescription;
+ dynamic clinicDescriptionN;
+ dynamic licenseExpiry;
+ int? employmentType;
+ dynamic setupID;
+ int? projectID;
+ String? projectName;
+ String? nationalityID;
+ String? nationalityName;
+ dynamic nationalityNameN;
+ int? gender;
+ String? genderDescription;
+ dynamic genderDescriptionN;
+ dynamic doctorTitle;
+ dynamic projectNameN;
+ bool? isAllowWaitList;
+ String? titleDescription;
+ dynamic titleDescriptionN;
+ dynamic isRegistered;
+ dynamic isDoctorDummy;
+ bool? isActive;
+ dynamic isDoctorAppointmentDisplayed;
+ bool? doctorClinicActive;
+ dynamic isbookingAllowed;
+ String? doctorCases;
+ dynamic doctorPicture;
+ String? doctorProfileInfo;
+ List? specialty;
+ num? actualDoctorRate;
+ String? consultationFee;
+ num? decimalDoctorRate;
+ String? doctorImageURL;
+ String? doctorMobileNumber;
+ num? doctorRate;
+ num? doctorStarsRate;
+ String? doctorTitleForProfile;
+ bool? isAppointmentAllowed;
+ bool? isDoctorHasPrePostImages;
+ String? nationalityFlagURL;
+ num? noOfPatientsRate;
+ String? qR;
+ int? serviceID;
+
+ DoctorsProfileResponseModel(
+ {this.doctorID,
+ this.doctorName,
+ this.doctorNameN,
+ this.clinicID,
+ this.clinicDescription,
+ this.clinicDescriptionN,
+ this.licenseExpiry,
+ this.employmentType,
+ this.setupID,
+ this.projectID,
+ this.projectName,
+ this.nationalityID,
+ this.nationalityName,
+ this.nationalityNameN,
+ this.gender,
+ this.genderDescription,
+ this.genderDescriptionN,
+ this.doctorTitle,
+ this.projectNameN,
+ this.isAllowWaitList,
+ this.titleDescription,
+ this.titleDescriptionN,
+ this.isRegistered,
+ this.isDoctorDummy,
+ this.isActive,
+ this.isDoctorAppointmentDisplayed,
+ this.doctorClinicActive,
+ this.isbookingAllowed,
+ this.doctorCases,
+ this.doctorPicture,
+ this.doctorProfileInfo,
+ this.specialty,
+ this.actualDoctorRate,
+ this.consultationFee,
+ this.decimalDoctorRate,
+ this.doctorImageURL,
+ this.doctorMobileNumber,
+ this.doctorRate,
+ this.doctorStarsRate,
+ this.doctorTitleForProfile,
+ this.isAppointmentAllowed,
+ this.isDoctorHasPrePostImages,
+ this.nationalityFlagURL,
+ this.noOfPatientsRate,
+ this.qR,
+ this.serviceID});
+
+ DoctorsProfileResponseModel.fromJson(Map json) {
+ doctorID = json['DoctorID'];
+ doctorName = json['DoctorName'];
+ doctorNameN = json['DoctorNameN'];
+ clinicID = json['ClinicID'];
+ clinicDescription = json['ClinicDescription'];
+ clinicDescriptionN = json['ClinicDescriptionN'];
+ licenseExpiry = json['LicenseExpiry'];
+ employmentType = json['EmploymentType'];
+ setupID = json['SetupID'];
+ projectID = json['ProjectID'];
+ projectName = json['ProjectName'];
+ nationalityID = json['NationalityID'];
+ nationalityName = json['NationalityName'];
+ nationalityNameN = json['NationalityNameN'];
+ gender = json['Gender'];
+ genderDescription = json['Gender_Description'];
+ genderDescriptionN = json['Gender_DescriptionN'];
+ doctorTitle = json['DoctorTitle'];
+ projectNameN = json['ProjectNameN'];
+ isAllowWaitList = json['IsAllowWaitList'];
+ titleDescription = json['Title_Description'];
+ titleDescriptionN = json['Title_DescriptionN'];
+ isRegistered = json['IsRegistered'];
+ isDoctorDummy = json['IsDoctorDummy'];
+ isActive = json['IsActive'];
+ isDoctorAppointmentDisplayed = json['IsDoctorAppointmentDisplayed'];
+ doctorClinicActive = json['DoctorClinicActive'];
+ isbookingAllowed = json['IsbookingAllowed'];
+ doctorCases = json['DoctorCases'];
+ doctorPicture = json['DoctorPicture'];
+ doctorProfileInfo = json['DoctorProfileInfo'];
+ specialty = json['Specialty'].cast();
+ actualDoctorRate = json['ActualDoctorRate'];
+ consultationFee = json['ConsultationFee'];
+ decimalDoctorRate = json['DecimalDoctorRate'];
+ doctorImageURL = json['DoctorImageURL'];
+ doctorMobileNumber = json['DoctorMobileNumber'];
+ doctorRate = json['DoctorRate'];
+ doctorStarsRate = json['DoctorStarsRate'];
+ doctorTitleForProfile = json['DoctorTitleForProfile'];
+ isAppointmentAllowed = json['IsAppointmentAllowed'];
+ isDoctorHasPrePostImages = json['IsDoctorHasPrePostImages'];
+ nationalityFlagURL = json['NationalityFlagURL'];
+ noOfPatientsRate = json['NoOfPatientsRate'];
+ qR = json['QR'];
+ serviceID = json['ServiceID'];
+ }
+
+ Map toJson() {
+ final Map data = new Map();
+ data['DoctorID'] = this.doctorID;
+ data['DoctorName'] = this.doctorName;
+ data['DoctorNameN'] = this.doctorNameN;
+ data['ClinicID'] = this.clinicID;
+ data['ClinicDescription'] = this.clinicDescription;
+ data['ClinicDescriptionN'] = this.clinicDescriptionN;
+ data['LicenseExpiry'] = this.licenseExpiry;
+ data['EmploymentType'] = this.employmentType;
+ data['SetupID'] = this.setupID;
+ data['ProjectID'] = this.projectID;
+ data['ProjectName'] = this.projectName;
+ data['NationalityID'] = this.nationalityID;
+ data['NationalityName'] = this.nationalityName;
+ data['NationalityNameN'] = this.nationalityNameN;
+ data['Gender'] = this.gender;
+ data['Gender_Description'] = this.genderDescription;
+ data['Gender_DescriptionN'] = this.genderDescriptionN;
+ data['DoctorTitle'] = this.doctorTitle;
+ data['ProjectNameN'] = this.projectNameN;
+ data['IsAllowWaitList'] = this.isAllowWaitList;
+ data['Title_Description'] = this.titleDescription;
+ data['Title_DescriptionN'] = this.titleDescriptionN;
+ data['IsRegistered'] = this.isRegistered;
+ data['IsDoctorDummy'] = this.isDoctorDummy;
+ data['IsActive'] = this.isActive;
+ data['IsDoctorAppointmentDisplayed'] = this.isDoctorAppointmentDisplayed;
+ data['DoctorClinicActive'] = this.doctorClinicActive;
+ data['IsbookingAllowed'] = this.isbookingAllowed;
+ data['DoctorCases'] = this.doctorCases;
+ data['DoctorPicture'] = this.doctorPicture;
+ data['DoctorProfileInfo'] = this.doctorProfileInfo;
+ data['Specialty'] = this.specialty;
+ data['ActualDoctorRate'] = this.actualDoctorRate;
+ data['ConsultationFee'] = this.consultationFee;
+ data['DecimalDoctorRate'] = this.decimalDoctorRate;
+ data['DoctorImageURL'] = this.doctorImageURL;
+ data['DoctorMobileNumber'] = this.doctorMobileNumber;
+ data['DoctorRate'] = this.doctorRate;
+ data['DoctorStarsRate'] = this.doctorStarsRate;
+ data['DoctorTitleForProfile'] = this.doctorTitleForProfile;
+ data['IsAppointmentAllowed'] = this.isAppointmentAllowed;
+ data['IsDoctorHasPrePostImages'] = this.isDoctorHasPrePostImages;
+ data['NationalityFlagURL'] = this.nationalityFlagURL;
+ data['NoOfPatientsRate'] = this.noOfPatientsRate;
+ data['QR'] = this.qR;
+ data['ServiceID'] = this.serviceID;
+ return data;
+ }
+}
diff --git a/lib/features/book_appointments/models/resp_models/doctors_list_response_model.dart b/lib/features/book_appointments/models/resp_models/doctors_list_response_model.dart
new file mode 100644
index 0000000..51a9403
--- /dev/null
+++ b/lib/features/book_appointments/models/resp_models/doctors_list_response_model.dart
@@ -0,0 +1,252 @@
+class DoctorsListResponseModel {
+ int? clinicID;
+ String? clinicName;
+ String? clinicNameN;
+ String? doctorTitle;
+ int? iD;
+ String? name;
+ int? projectID;
+ String? projectName;
+ num? actualDoctorRate;
+ int? clinicRoomNo;
+ dynamic date;
+ dynamic dayName;
+ num? decimalDoctorRate;
+ dynamic doctorAvailability;
+ int? doctorID;
+ String? doctorImageURL;
+ String? doctorMobileNumber;
+ dynamic doctorProfile;
+ dynamic doctorProfileInfo;
+ num? doctorRate;
+ num? doctorStarsRate;
+ int? employmentType;
+ int? gender;
+ String? genderDescription;
+ int? hISRegionId;
+ bool? isActive;
+ bool? isAllowWaitList;
+ bool? isAppointmentAllowed;
+ bool? isDoctorAllowVedioCall;
+ bool? isDoctorDummy;
+ bool? isDoctorHasPrePostImages;
+ bool? isHMC;
+ bool? isHmg;
+ bool? isLiveCare;
+ String? latitude;
+ String? longitude;
+ String? nationalityFlagURL;
+ String? nationalityID;
+ String? nationalityName;
+ dynamic nearestFreeSlot;
+ int? noOfFreeSlotsAvailable;
+ num? noOfPatientsRate;
+ dynamic originalClinicID;
+ int? personRate;
+ num? projectDistanceInKiloMeters;
+ String? projectNameBottom;
+ String? projectNameTop;
+ String? qR;
+ dynamic qRString;
+ num? rateNumber;
+ String? regionName;
+ dynamic regionNameN;
+ dynamic serviceID;
+ String? setupID;
+ List? speciality;
+ List? specialityN;
+ int? transactionType;
+ int? virtualEmploymentType;
+ dynamic workingHours;
+ dynamic vida3Id;
+
+ DoctorsListResponseModel(
+ {this.clinicID,
+ this.clinicName,
+ this.clinicNameN,
+ this.doctorTitle,
+ this.iD,
+ this.name,
+ this.projectID,
+ this.projectName,
+ this.actualDoctorRate,
+ this.clinicRoomNo,
+ this.date,
+ this.dayName,
+ this.decimalDoctorRate,
+ this.doctorAvailability,
+ this.doctorID,
+ this.doctorImageURL,
+ this.doctorMobileNumber,
+ this.doctorProfile,
+ this.doctorProfileInfo,
+ this.doctorRate,
+ this.doctorStarsRate,
+ this.employmentType,
+ this.gender,
+ this.genderDescription,
+ this.hISRegionId,
+ this.isActive,
+ this.isAllowWaitList,
+ this.isAppointmentAllowed,
+ this.isDoctorAllowVedioCall,
+ this.isDoctorDummy,
+ this.isDoctorHasPrePostImages,
+ this.isHMC,
+ this.isHmg,
+ this.isLiveCare,
+ this.latitude,
+ this.longitude,
+ this.nationalityFlagURL,
+ this.nationalityID,
+ this.nationalityName,
+ this.nearestFreeSlot,
+ this.noOfFreeSlotsAvailable,
+ this.noOfPatientsRate,
+ this.originalClinicID,
+ this.personRate,
+ this.projectDistanceInKiloMeters,
+ this.projectNameBottom,
+ this.projectNameTop,
+ this.qR,
+ this.qRString,
+ this.rateNumber,
+ this.regionName,
+ this.regionNameN,
+ this.serviceID,
+ this.setupID,
+ this.speciality,
+ this.specialityN,
+ this.transactionType,
+ this.virtualEmploymentType,
+ this.workingHours,
+ this.vida3Id});
+
+ DoctorsListResponseModel.fromJson(Map json) {
+ clinicID = json['ClinicID'];
+ clinicName = json['ClinicName'];
+ clinicNameN = json['ClinicNameN'];
+ doctorTitle = json['DoctorTitle'];
+ iD = json['ID'];
+ name = json['Name'];
+ projectID = json['ProjectID'];
+ projectName = json['ProjectName'];
+ actualDoctorRate = json['ActualDoctorRate'];
+ clinicRoomNo = json['ClinicRoomNo'];
+ date = json['Date'];
+ dayName = json['DayName'];
+ decimalDoctorRate = json['DecimalDoctorRate'];
+ doctorAvailability = json['DoctorAvailability'];
+ doctorID = json['DoctorID'];
+ doctorImageURL = json['DoctorImageURL'];
+ doctorMobileNumber = json['DoctorMobileNumber'];
+ doctorProfile = json['DoctorProfile'];
+ doctorProfileInfo = json['DoctorProfileInfo'];
+ doctorRate = json['DoctorRate'];
+ doctorStarsRate = json['DoctorStarsRate'];
+ employmentType = json['EmploymentType'];
+ gender = json['Gender'];
+ genderDescription = json['GenderDescription'];
+ hISRegionId = json['HISRegionId'];
+ isActive = json['IsActive'];
+ isAllowWaitList = json['IsAllowWaitList'];
+ isAppointmentAllowed = json['IsAppointmentAllowed'];
+ isDoctorAllowVedioCall = json['IsDoctorAllowVedioCall'];
+ isDoctorDummy = json['IsDoctorDummy'];
+ isDoctorHasPrePostImages = json['IsDoctorHasPrePostImages'];
+ isHMC = json['IsHMC'];
+ isHmg = json['IsHmg'];
+ isLiveCare = json['IsLiveCare'];
+ latitude = json['Latitude'];
+ longitude = json['Longitude'];
+ nationalityFlagURL = json['NationalityFlagURL'];
+ nationalityID = json['NationalityID'];
+ nationalityName = json['NationalityName'];
+ nearestFreeSlot = json['NearestFreeSlot'];
+ noOfFreeSlotsAvailable = json['NoOfFreeSlotsAvailable'];
+ noOfPatientsRate = json['NoOfPatientsRate'];
+ originalClinicID = json['OriginalClinicID'];
+ personRate = json['PersonRate'];
+ projectDistanceInKiloMeters = json['ProjectDistanceInKiloMeters'];
+ projectNameBottom = json['ProjectNameBottom'];
+ projectNameTop = json['ProjectNameTop'];
+ qR = json['QR'];
+ qRString = json['QRString'];
+ rateNumber = json['RateNumber'];
+ regionName = json['RegionName'];
+ regionNameN = json['RegionNameN'];
+ serviceID = json['ServiceID'];
+ setupID = json['SetupID'];
+ speciality = json['Speciality'] != null ? json['Speciality'].cast() : [];
+ // specialityN = json['SpecialityN'].cast();
+ transactionType = json['TransactionType'];
+ virtualEmploymentType = json['VirtualEmploymentType'];
+ workingHours = json['WorkingHours'];
+ vida3Id = json['vida3Id'];
+ }
+
+ Map toJson() {
+ final Map data = new Map();
+ data['ClinicID'] = this.clinicID;
+ data['ClinicName'] = this.clinicName;
+ data['ClinicNameN'] = this.clinicNameN;
+ data['DoctorTitle'] = this.doctorTitle;
+ data['ID'] = this.iD;
+ data['Name'] = this.name;
+ data['ProjectID'] = this.projectID;
+ data['ProjectName'] = this.projectName;
+ data['ActualDoctorRate'] = this.actualDoctorRate;
+ data['ClinicRoomNo'] = this.clinicRoomNo;
+ data['Date'] = this.date;
+ data['DayName'] = this.dayName;
+ data['DecimalDoctorRate'] = this.decimalDoctorRate;
+ data['DoctorAvailability'] = this.doctorAvailability;
+ data['DoctorID'] = this.doctorID;
+ data['DoctorImageURL'] = this.doctorImageURL;
+ data['DoctorMobileNumber'] = this.doctorMobileNumber;
+ data['DoctorProfile'] = this.doctorProfile;
+ data['DoctorProfileInfo'] = this.doctorProfileInfo;
+ data['DoctorRate'] = this.doctorRate;
+ data['DoctorStarsRate'] = this.doctorStarsRate;
+ data['EmploymentType'] = this.employmentType;
+ data['Gender'] = this.gender;
+ data['GenderDescription'] = this.genderDescription;
+ data['HISRegionId'] = this.hISRegionId;
+ data['IsActive'] = this.isActive;
+ data['IsAllowWaitList'] = this.isAllowWaitList;
+ data['IsAppointmentAllowed'] = this.isAppointmentAllowed;
+ data['IsDoctorAllowVedioCall'] = this.isDoctorAllowVedioCall;
+ data['IsDoctorDummy'] = this.isDoctorDummy;
+ data['IsDoctorHasPrePostImages'] = this.isDoctorHasPrePostImages;
+ data['IsHMC'] = this.isHMC;
+ data['IsHmg'] = this.isHmg;
+ data['IsLiveCare'] = this.isLiveCare;
+ data['Latitude'] = this.latitude;
+ data['Longitude'] = this.longitude;
+ data['NationalityFlagURL'] = this.nationalityFlagURL;
+ data['NationalityID'] = this.nationalityID;
+ data['NationalityName'] = this.nationalityName;
+ data['NearestFreeSlot'] = this.nearestFreeSlot;
+ data['NoOfFreeSlotsAvailable'] = this.noOfFreeSlotsAvailable;
+ data['NoOfPatientsRate'] = this.noOfPatientsRate;
+ data['OriginalClinicID'] = this.originalClinicID;
+ data['PersonRate'] = this.personRate;
+ data['ProjectDistanceInKiloMeters'] = this.projectDistanceInKiloMeters;
+ data['ProjectNameBottom'] = this.projectNameBottom;
+ data['ProjectNameTop'] = this.projectNameTop;
+ data['QR'] = this.qR;
+ data['QRString'] = this.qRString;
+ data['RateNumber'] = this.rateNumber;
+ data['RegionName'] = this.regionName;
+ data['RegionNameN'] = this.regionNameN;
+ data['ServiceID'] = this.serviceID;
+ data['SetupID'] = this.setupID;
+ data['Speciality'] = this.speciality;
+ data['SpecialityN'] = this.specialityN;
+ data['TransactionType'] = this.transactionType;
+ data['VirtualEmploymentType'] = this.virtualEmploymentType;
+ data['WorkingHours'] = this.workingHours;
+ data['vida3Id'] = this.vida3Id;
+ return data;
+ }
+}
diff --git a/lib/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart b/lib/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart
new file mode 100644
index 0000000..0b0fa13
--- /dev/null
+++ b/lib/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart
@@ -0,0 +1,36 @@
+class GetClinicsListResponseModel {
+ int? clinicID;
+ String? clinicDescription;
+ String? clinicDescriptionN;
+ int? age;
+ int? gender;
+ bool? isLiveCareClinicAndOnline;
+ int? liveCareClinicID;
+ int? liveCareServiceID;
+
+ GetClinicsListResponseModel({this.clinicID, this.clinicDescription, this.clinicDescriptionN, this.age, this.gender, this.isLiveCareClinicAndOnline, this.liveCareClinicID, this.liveCareServiceID});
+
+ GetClinicsListResponseModel.fromJson(Map json) {
+ clinicID = json['ClinicID'];
+ clinicDescription = json['ClinicDescription'];
+ clinicDescriptionN = json['ClinicDescriptionN'];
+ age = json['Age'];
+ gender = json['Gender'];
+ isLiveCareClinicAndOnline = json['IsLiveCareClinicAndOnline'];
+ liveCareClinicID = json['LiveCareClinicID'];
+ liveCareServiceID = json['LiveCareServiceID'];
+ }
+
+ Map toJson() {
+ final Map data = new Map();
+ data['ClinicID'] = this.clinicID;
+ data['ClinicDescription'] = this.clinicDescription;
+ data['ClinicDescriptionN'] = this.clinicDescriptionN;
+ data['Age'] = this.age;
+ data['Gender'] = this.gender;
+ data['IsLiveCareClinicAndOnline'] = this.isLiveCareClinicAndOnline;
+ data['LiveCareClinicID'] = this.liveCareClinicID;
+ data['LiveCareServiceID'] = this.liveCareServiceID;
+ return data;
+ }
+}
diff --git a/lib/features/book_appointments/models/timeslots.dart b/lib/features/book_appointments/models/timeslots.dart
new file mode 100644
index 0000000..8fc7b15
--- /dev/null
+++ b/lib/features/book_appointments/models/timeslots.dart
@@ -0,0 +1,9 @@
+class TimeSlot {
+ String? isoTime;
+ DateTime? start;
+ DateTime? end;
+ String? vidaDate;
+ bool isSelected = false;
+
+ TimeSlot({required this.isoTime, required this.start, required this.end, this.vidaDate});
+}
diff --git a/lib/main.dart b/lib/main.dart
index 24f5c36..7a5e2e4 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -128,6 +128,8 @@ void main() async {
create: (_) => BookAppointmentsViewModel(
bookAppointmentsRepo: getIt(),
errorHandlerService: getIt(),
+ navigationService: getIt(),
+ myAppointmentsViewModel: getIt(),
),
),
ChangeNotifierProvider(
diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart
index ee2a042..42ace37 100644
--- a/lib/presentation/appointments/appointment_details_page.dart
+++ b/lib/presentation/appointments/appointment_details_page.dart
@@ -9,6 +9,8 @@ import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
+import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart';
+import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/utils/appointment_type.dart';
@@ -18,12 +20,14 @@ import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/appointments/appointment_payment_page.dart';
import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart';
import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_doctor_card.dart';
+import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart';
import 'package:hmg_patient_app_new/presentation/lab/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_detail_page.dart';
import 'package:hmg_patient_app_new/presentation/prescriptions/prescriptions_list_page.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
+import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/shimmer/movies_shimmer_widget.dart';
import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart';
import 'package:maps_launcher/maps_launcher.dart';
@@ -43,6 +47,7 @@ class AppointmentDetailsPage extends StatefulWidget {
class _AppointmentDetailsPageState extends State {
late MyAppointmentsViewModel myAppointmentsViewModel;
late PrescriptionsViewModel prescriptionsViewModel;
+ late BookAppointmentsViewModel bookAppointmentsViewModel;
@override
void initState() {
@@ -59,6 +64,7 @@ class _AppointmentDetailsPageState extends State {
Widget build(BuildContext context) {
myAppointmentsViewModel = Provider.of(context, listen: false);
prescriptionsViewModel = Provider.of(context, listen: false);
+ bookAppointmentsViewModel = Provider.of(context, listen: false);
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: Column(
@@ -123,7 +129,9 @@ class _AppointmentDetailsPageState extends State {
Navigator.of(context).pop();
Navigator.of(context).pop();
},
- onRescheduleTap: () {},
+ onRescheduleTap: () async {
+ openDoctorScheduleCalendar();
+ },
),
SizedBox(height: 16.h),
!AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)
@@ -443,7 +451,9 @@ class _AppointmentDetailsPageState extends State {
AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)
? CustomButton(
text: "Re-book Appointment".needTranslation,
- onPressed: () {},
+ onPressed: () {
+ openDoctorScheduleCalendar();
+ },
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
@@ -482,6 +492,46 @@ class _AppointmentDetailsPageState extends State {
);
}
+ openDoctorScheduleCalendar() async {
+ DoctorsListResponseModel doctor = DoctorsListResponseModel(
+ clinicID: widget.patientAppointmentHistoryResponseModel.clinicID,
+ projectID: widget.patientAppointmentHistoryResponseModel.projectID,
+ doctorID: widget.patientAppointmentHistoryResponseModel.doctorID,
+ doctorImageURL: widget.patientAppointmentHistoryResponseModel.doctorImageURL,
+ doctorTitle: widget.patientAppointmentHistoryResponseModel.doctorTitle,
+ name: widget.patientAppointmentHistoryResponseModel.doctorNameObj,
+ nationalityFlagURL: "https://hmgwebservices.com/Images/flag/SYR.png",
+ speciality: [],
+ clinicName: widget.patientAppointmentHistoryResponseModel.clinicName,
+ projectName: widget.patientAppointmentHistoryResponseModel.projectName,
+ );
+ bookAppointmentsViewModel.setSelectedDoctor(doctor);
+ LoaderBottomSheet.showLoader();
+ await bookAppointmentsViewModel.getDoctorFreeSlots(
+ isBookingForLiveCare: false,
+ onSuccess: (dynamic respData) async {
+ LoaderBottomSheet.hideLoader();
+ showCommonBottomSheetWithoutHeight(
+ title: "Pick a Date".needTranslation,
+ context,
+ child: AppointmentCalendar(),
+ isFullScreen: false,
+ isCloseButtonVisible: true,
+ callBackFunc: () {},
+ );
+ },
+ onError: (err) {
+ LoaderBottomSheet.hideLoader();
+ showCommonBottomSheetWithoutHeight(
+ context,
+ child: Utils.getErrorWidget(loadingText: err),
+ callBackFunc: () {},
+ isFullScreen: false,
+ isCloseButtonVisible: true,
+ );
+ });
+ }
+
Future handleAppointmentNextAction(nextAction) async {
switch (nextAction) {
case 0:
diff --git a/lib/presentation/appointments/widgets/appointment_doctor_card.dart b/lib/presentation/appointments/widgets/appointment_doctor_card.dart
index d7b0f68..da00d88 100644
--- a/lib/presentation/appointments/widgets/appointment_doctor_card.dart
+++ b/lib/presentation/appointments/widgets/appointment_doctor_card.dart
@@ -97,7 +97,9 @@ class AppointmentDoctorCard extends StatelessWidget {
)
: CustomButton(
text: "Rebook with same doctor".needTranslation,
- onPressed: () {},
+ onPressed: () {
+ onRescheduleTap();
+ },
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
diff --git a/lib/presentation/authentication/saved_login_screen.dart b/lib/presentation/authentication/saved_login_screen.dart
index 9f55c85..2127647 100644
--- a/lib/presentation/authentication/saved_login_screen.dart
+++ b/lib/presentation/authentication/saved_login_screen.dart
@@ -310,6 +310,7 @@ class _SavedLogin extends State {
2: AppAssets.fingerprint,
3: AppAssets.fingerprint,
4: AppAssets.whatsapp,
+ 0: AppAssets.sms,
};
if (types.containsKey(type)) {
diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart
index 0f9a325..9533d6c 100644
--- a/lib/presentation/book_appointment/book_appointment_page.dart
+++ b/lib/presentation/book_appointment/book_appointment_page.dart
@@ -1,3 +1,5 @@
+import 'dart:async';
+
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
@@ -9,6 +11,7 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
+import 'package:hmg_patient_app_new/presentation/book_appointment/search_doctor_by_name.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/select_clinic_page.dart';
import 'package:hmg_patient_app_new/presentation/lab/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
@@ -25,14 +28,25 @@ class BookAppointmentPage extends StatefulWidget {
class _BookAppointmentPageState extends State {
late AppState appState;
+ late BookAppointmentsViewModel bookAppointmentsViewModel;
+
+ @override
+ void initState() {
+ scheduleMicrotask(() {
+ bookAppointmentsViewModel.initBookAppointmentViewModel();
+ });
+ super.initState();
+ }
@override
Widget build(BuildContext context) {
+ bookAppointmentsViewModel = Provider.of(context, listen: false);
appState = getIt.get();
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
title: LocaleKeys.bookAppo.tr(context: context),
+ isLeading: false,
child: SingleChildScrollView(
child: Consumer(builder: (context, bookAppointmentsVM, child) {
return Column(
@@ -95,6 +109,7 @@ class _BookAppointmentPageState extends State {
Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 15.h, height: 15.h),
],
).onPress(() {
+ bookAppointmentsViewModel.setIsClinicsListLoading(true);
Navigator.of(context).push(
FadePage(
page: SelectClinicPage(),
@@ -122,7 +137,14 @@ class _BookAppointmentPageState extends State {
),
Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 15.h, height: 15.h),
],
- ).onPress(() {}),
+ ).onPress(() {
+ bookAppointmentsViewModel.setIsDoctorSearchByNameStarted(false);
+ Navigator.of(context).push(
+ FadePage(
+ page: SearchDoctorByName(),
+ ),
+ );
+ }),
SizedBox(height: 16.h),
Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h),
SizedBox(height: 16.h),
diff --git a/lib/presentation/book_appointment/doctor_profile_page.dart b/lib/presentation/book_appointment/doctor_profile_page.dart
new file mode 100644
index 0000000..8640c79
--- /dev/null
+++ b/lib/presentation/book_appointment/doctor_profile_page.dart
@@ -0,0 +1,159 @@
+import 'dart:math';
+
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:hijri_gregorian_calendar/hijri_gregorian_calendar.dart';
+import 'package:hmg_patient_app_new/core/app_assets.dart';
+import 'package:hmg_patient_app_new/core/app_state.dart';
+import 'package:hmg_patient_app_new/core/dependencies.dart';
+import 'package:hmg_patient_app_new/core/utils/date_util.dart';
+import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
+import 'package:hmg_patient_app_new/core/utils/utils.dart';
+import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
+import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
+import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart';
+import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
+import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart';
+import 'package:hmg_patient_app_new/presentation/lab/collapsing_list_view.dart';
+import 'package:hmg_patient_app_new/theme/colors.dart';
+import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
+import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
+import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
+import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
+import 'package:provider/provider.dart';
+
+class DoctorProfilePage extends StatelessWidget {
+ DoctorProfilePage({super.key});
+
+ late AppState appState;
+ late BookAppointmentsViewModel bookAppointmentsViewModel;
+
+ @override
+ Widget build(BuildContext context) {
+ bookAppointmentsViewModel = Provider.of(context, listen: false);
+ appState = getIt.get();
+ return Scaffold(
+ backgroundColor: AppColors.bgScaffoldColor,
+ body: Column(
+ children: [
+ Expanded(
+ child: CollapsingListView(
+ title: "Doctor Profile".needTranslation,
+ child: SingleChildScrollView(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SizedBox(height: 24.h),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Row(
+ children: [
+ Image.network(
+ bookAppointmentsViewModel.doctorsProfileResponseModel.doctorImageURL!,
+ width: 63.h,
+ height: 63.h,
+ fit: BoxFit.fill,
+ ).circle(100),
+ SizedBox(width: 8.h),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ ("${bookAppointmentsViewModel.doctorsProfileResponseModel.doctorTitleForProfile} ${bookAppointmentsViewModel.doctorsProfileResponseModel.doctorName}")
+ .toString()
+ .toText28(isBold: true),
+ (bookAppointmentsViewModel.doctorsProfileResponseModel.specialty!.isNotEmpty ? bookAppointmentsViewModel.doctorsProfileResponseModel.specialty!.first : "")
+ .toString()
+ .toText18(weight: FontWeight.w500, color: AppColors.primaryRedColor),
+ ],
+ ),
+ ],
+ ),
+ Image.network(
+ bookAppointmentsViewModel.doctorsProfileResponseModel.nationalityFlagURL!,
+ width: 32.h,
+ height: 32.h,
+ fit: BoxFit.fill,
+ ),
+ ],
+ ),
+ SizedBox(height: 12.h),
+ Wrap(
+ direction: Axis.horizontal,
+ spacing: 3.h,
+ runSpacing: 4.h,
+ children: [
+ AppCustomChipWidget(
+ icon: AppAssets.rating_icon,
+ iconColor: AppColors.ratingColorYellow,
+ labelText: "Rating: ${bookAppointmentsViewModel.doctorsProfileResponseModel.decimalDoctorRate}".needTranslation,
+ ),
+ AppCustomChipWidget(
+ icon: AppAssets.rating_icon,
+ iconColor: AppColors.ratingColorYellow,
+ labelText: "Reviews: ${bookAppointmentsViewModel.doctorsProfileResponseModel.noOfPatientsRate}".needTranslation,
+ ),
+ ],
+ ),
+ SizedBox(height: 16.h),
+ Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h),
+ SizedBox(height: 16.h),
+ "Biography".toText14(weight: FontWeight.w600, color: AppColors.textColor),
+ bookAppointmentsViewModel.doctorsProfileResponseModel.doctorProfileInfo!.toText12(fontWeight: FontWeight.w600, color: AppColors.greyTextColor),
+ ],
+ ).paddingSymmetrical(24.h, 0.h),
+ ),
+ ),
+ ),
+ Container(
+ decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
+ color: AppColors.whiteColor,
+ borderRadius: 24.h,
+ hasShadow: true,
+ ),
+ child: CustomButton(
+ text: "View available appointments".needTranslation,
+ onPressed: () async {
+ LoaderBottomSheet.showLoader();
+ await bookAppointmentsViewModel.getDoctorFreeSlots(
+ isBookingForLiveCare: false,
+ onSuccess: (dynamic respData) async {
+ LoaderBottomSheet.hideLoader();
+ showCommonBottomSheetWithoutHeight(
+ title: "Pick a Date".needTranslation,
+ context,
+ child: AppointmentCalendar(),
+ isFullScreen: false,
+ isCloseButtonVisible: true,
+ callBackFunc: () {},
+ );
+ },
+ onError: (err) {
+ LoaderBottomSheet.hideLoader();
+ showCommonBottomSheetWithoutHeight(
+ context,
+ child: Utils.getErrorWidget(loadingText: err),
+ callBackFunc: () {},
+ isFullScreen: false,
+ isCloseButtonVisible: true,
+ );
+ });
+ },
+ backgroundColor: AppColors.primaryRedColor,
+ borderColor: AppColors.primaryRedColor,
+ textColor: AppColors.whiteColor,
+ fontSize: 16,
+ fontWeight: FontWeight.w500,
+ borderRadius: 12,
+ padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
+ height: 50.h,
+ icon: AppAssets.calendar,
+ iconColor: AppColors.whiteColor,
+ iconSize: 20.h,
+ ).paddingSymmetrical(24.h, 24.h),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/presentation/book_appointment/review_appointment_page.dart b/lib/presentation/book_appointment/review_appointment_page.dart
new file mode 100644
index 0000000..bd43a9f
--- /dev/null
+++ b/lib/presentation/book_appointment/review_appointment_page.dart
@@ -0,0 +1,236 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:hmg_patient_app_new/core/app_assets.dart';
+import 'package:hmg_patient_app_new/core/app_state.dart';
+import 'package:hmg_patient_app_new/core/dependencies.dart';
+import 'package:hmg_patient_app_new/core/utils/loading_utils.dart';
+import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
+import 'package:hmg_patient_app_new/core/utils/utils.dart';
+import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
+import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
+import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart';
+import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart';
+import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
+import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart';
+import 'package:hmg_patient_app_new/presentation/lab/collapsing_list_view.dart';
+import 'package:hmg_patient_app_new/theme/colors.dart';
+import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
+import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
+import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart';
+import 'package:provider/provider.dart';
+
+class ReviewAppointmentPage extends StatefulWidget {
+ const ReviewAppointmentPage({super.key});
+
+ @override
+ State createState() => _ReviewAppointmentPageState();
+}
+
+class _ReviewAppointmentPageState extends State {
+ late AppState appState;
+ late BookAppointmentsViewModel bookAppointmentsViewModel;
+ late AuthenticationViewModel authVM;
+
+ @override
+ Widget build(BuildContext context) {
+ bookAppointmentsViewModel = Provider.of(context, listen: false);
+ authVM = Provider.of(context, listen: false);
+ appState = getIt.get();
+ return Scaffold(
+ backgroundColor: AppColors.scaffoldBgColor,
+ body: Column(
+ children: [
+ Expanded(
+ child: CollapsingListView(
+ title: LocaleKeys.reviewAppointment.tr(context: context),
+ child: SingleChildScrollView(
+ padding: EdgeInsets.symmetric(horizontal: 24.h),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SizedBox(height: 24.h),
+ LocaleKeys.docInfo.tr(context: context).toText16(isBold: true),
+ SizedBox(height: 16.h),
+ Container(
+ decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
+ color: AppColors.whiteColor,
+ borderRadius: 24.h,
+ hasShadow: false,
+ ),
+ child: Padding(
+ padding: EdgeInsets.all(16.h),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Image.network(
+ bookAppointmentsViewModel.selectedDoctor.doctorImageURL!,
+ width: 50.h,
+ height: 50.h,
+ fit: BoxFit.fill,
+ ).circle(100),
+ SizedBox(width: 8.h),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ SizedBox(
+ width: MediaQuery.of(context).size.width * 0.49,
+ child:
+ "${bookAppointmentsViewModel.selectedDoctor.doctorTitle} ${bookAppointmentsViewModel.selectedDoctor.name}".toString().toText16(isBold: true, maxlines: 1),
+ ),
+ Image.network(
+ bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL!,
+ width: 20.h,
+ height: 15.h,
+ fit: BoxFit.fill,
+ ),
+ ],
+ ),
+ SizedBox(height: 2.h),
+ (bookAppointmentsViewModel.selectedDoctor.speciality!.isNotEmpty ? bookAppointmentsViewModel.selectedDoctor.speciality!.first : "")
+ .toString()
+ .toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor, maxLine: 1),
+ ],
+ ),
+ ],
+ ),
+ SizedBox(height: 12.h),
+ Wrap(
+ direction: Axis.horizontal,
+ spacing: 8.h,
+ runSpacing: 8.h,
+ children: [
+ AppCustomChipWidget(
+ labelText: "${LocaleKeys.clinic.tr(context: context)}: ${bookAppointmentsViewModel.selectedDoctor.clinicName}".needTranslation,
+ ),
+ AppCustomChipWidget(
+ labelText: "${LocaleKeys.branch.tr(context: context)} ${bookAppointmentsViewModel.selectedDoctor.projectName}".needTranslation,
+ ),
+ AppCustomChipWidget(
+ labelText: "${LocaleKeys.date.tr(context: context)}: ${bookAppointmentsViewModel.selectedAppointmentDate}".needTranslation,
+ ),
+ AppCustomChipWidget(
+ labelText: "${LocaleKeys.time.tr(context: context)}: ${bookAppointmentsViewModel.selectedAppointmentTime}".needTranslation,
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ SizedBox(height: 24.h),
+ LocaleKeys.patientInfo.tr(context: context).toText16(isBold: true),
+ SizedBox(height: 16.h),
+ Container(
+ decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
+ color: AppColors.whiteColor,
+ borderRadius: 24.h,
+ hasShadow: false,
+ ),
+ child: Padding(
+ padding: EdgeInsets.all(16.h),
+ child: Row(
+ children: [
+ Image.asset(
+ appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.male_img : AppAssets.femaleImg,
+ width: 52.h,
+ height: 52.h,
+ ),
+ SizedBox(width: 8.h),
+ Column(
+ children: [
+ "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText16(isBold: true),
+ SizedBox(height: 8.h),
+ AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} Years Old"),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ SizedBox(height: 24.h),
+ "Hospital Information".needTranslation.toText16(isBold: true),
+ SizedBox(height: 16.h),
+ Container(
+ decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
+ color: AppColors.whiteColor,
+ borderRadius: 12.h,
+ hasShadow: false,
+ ),
+ child: Padding(
+ padding: EdgeInsets.all(16.h),
+ child: Row(
+ children: [
+ bookAppointmentsViewModel.selectedDoctor.projectName!.toText16(isBold: true),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ Container(
+ decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
+ color: AppColors.whiteColor,
+ borderRadius: 24.h,
+ hasShadow: true,
+ ),
+ child: CustomButton(
+ text: LocaleKeys.bookAppo.tr(context: context),
+ onPressed: () async {
+ initiateBookAppointment();
+ },
+ backgroundColor: AppColors.primaryRedColor,
+ borderColor: AppColors.primaryRedColor,
+ textColor: AppColors.whiteColor,
+ fontSize: 16,
+ fontWeight: FontWeight.w500,
+ borderRadius: 12,
+ padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
+ height: 50.h,
+ ).paddingSymmetrical(24.h, 24.h),
+ ),
+ ],
+ ),
+ );
+ }
+
+ void initiateBookAppointment() async {
+ LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: false, loadingText: "Booking your appointment...".needTranslation);
+
+ await bookAppointmentsViewModel.insertSpecificAppointment(onError: (err) {
+ print(err.data["ErrorEndUserMessage"]);
+ LoadingUtils.hideFullScreenLoader();
+ }, onSuccess: (apiResp) async {
+ LoadingUtils.hideFullScreenLoader();
+ await Future.delayed(Duration(milliseconds: 50)).then((value) async {
+ LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: true, loadingText: LocaleKeys.appointmentSuccess.tr());
+ await Future.delayed(Duration(milliseconds: 4000)).then((value) {
+ LoadingUtils.hideFullScreenLoader();
+ Navigator.pushAndRemoveUntil(
+ context,
+ FadePage(
+ page: LandingNavigation(),
+ ),
+ (r) => false);
+ });
+ });
+ });
+
+ // await Future.delayed(Duration(milliseconds: 4000)).then((value) async {
+ // LoadingUtils.hideFullScreenLoader();
+
+ // await Future.delayed(Duration(milliseconds: 50)).then((value) async {
+ // LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: true, loadingText: LocaleKeys.appointmentSuccess.tr());
+ // await Future.delayed(Duration(milliseconds: 4000)).then((value) {
+ // LoadingUtils.hideFullScreenLoader();
+ // });
+ // });
+ // });
+ }
+}
diff --git a/lib/presentation/book_appointment/search_doctor_by_name.dart b/lib/presentation/book_appointment/search_doctor_by_name.dart
new file mode 100644
index 0000000..23c5c18
--- /dev/null
+++ b/lib/presentation/book_appointment/search_doctor_by_name.dart
@@ -0,0 +1,212 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
+import 'package:hmg_patient_app_new/core/app_assets.dart';
+import 'package:hmg_patient_app_new/core/app_state.dart';
+import 'package:hmg_patient_app_new/core/dependencies.dart';
+import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
+import 'package:hmg_patient_app_new/core/utils/utils.dart';
+import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
+import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
+import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart';
+import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
+import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_profile_page.dart';
+import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/doctor_card.dart';
+import 'package:hmg_patient_app_new/presentation/lab/collapsing_list_view.dart';
+import 'package:hmg_patient_app_new/theme/colors.dart';
+import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
+import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
+import 'package:hmg_patient_app_new/widgets/input_widget.dart';
+import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
+import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart';
+import 'package:provider/provider.dart';
+
+import '../../features/book_appointments/models/resp_models/doctors_list_response_model.dart';
+
+class SearchDoctorByName extends StatefulWidget {
+ const SearchDoctorByName({super.key});
+
+ @override
+ State createState() => _SearchDoctorByNameState();
+}
+
+class _SearchDoctorByNameState extends State {
+ TextEditingController searchEditingController = TextEditingController();
+
+ FocusNode textFocusNode = FocusNode();
+
+ late AppState appState;
+ late BookAppointmentsViewModel bookAppointmentsViewModel;
+
+ @override
+ Widget build(BuildContext context) {
+ bookAppointmentsViewModel = Provider.of(context, listen: false);
+ appState = getIt.get();
+ return Scaffold(
+ backgroundColor: AppColors.bgScaffoldColor,
+ body: Column(
+ children: [
+ Expanded(
+ child: CollapsingListView(
+ title: "Choose Doctor".needTranslation,
+ child: SingleChildScrollView(
+ child: Padding(
+ padding: EdgeInsets.symmetric(horizontal: 24.h),
+ child: Column(
+ children: [
+ SizedBox(height: 16.h),
+ TextInputWidget(
+ labelText: LocaleKeys.search.tr(context: context),
+ hintText: LocaleKeys.doctorName.tr(context: context),
+ controller: searchEditingController,
+ isEnable: true,
+ prefix: null,
+ autoFocus: false,
+ isBorderAllowed: false,
+ keyboardType: TextInputType.text,
+ focusNode: textFocusNode,
+ suffix: searchEditingController.text.isNotEmpty
+ ? GestureDetector(
+ onTap: () {
+ searchEditingController.clear();
+ // bookAppointmentsViewModel.filterClinics("");
+ textFocusNode.unfocus();
+ },
+ child: Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown),
+ )
+ : null,
+ onChange: (value) {
+ // bookAppointmentsViewModel.filterClinics(value!);
+ },
+ padding: EdgeInsets.symmetric(
+ vertical: ResponsiveExtension(10).h,
+ horizontal: ResponsiveExtension(15).h,
+ ),
+ ),
+ SizedBox(height: 16.h),
+ Consumer(builder: (context, bookAppointmentsVM, child) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ bookAppointmentsVM.isDoctorSearchByNameStarted
+ ? ListView.separated(
+ padding: EdgeInsets.only(top: 24.h),
+ shrinkWrap: true,
+ physics: NeverScrollableScrollPhysics(),
+ itemCount: bookAppointmentsVM.isDoctorsListLoading ? 5 : bookAppointmentsVM.doctorsList.length,
+ itemBuilder: (context, index) {
+ return bookAppointmentsVM.isDoctorsListLoading
+ ? DoctorCard(
+ doctorsListResponseModel: DoctorsListResponseModel(),
+ isLoading: true,
+ bookAppointmentsViewModel: bookAppointmentsViewModel,
+ )
+ : AnimationConfiguration.staggeredList(
+ position: index,
+ duration: const Duration(milliseconds: 500),
+ child: SlideAnimation(
+ verticalOffset: 100.0,
+ child: FadeInAnimation(
+ child: AnimatedContainer(
+ duration: Duration(milliseconds: 300),
+ curve: Curves.easeInOut,
+ decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true),
+ child: DoctorCard(
+ doctorsListResponseModel: bookAppointmentsVM.doctorsList[index],
+ isLoading: false,
+ bookAppointmentsViewModel: bookAppointmentsViewModel,
+ ).onPress(() async {
+ bookAppointmentsVM.setSelectedDoctor(bookAppointmentsVM.doctorsList[index]);
+ // bookAppointmentsVM.setSelectedDoctor(DoctorsListResponseModel());
+ LoaderBottomSheet.showLoader();
+ await bookAppointmentsVM.getDoctorProfile(onSuccess: (dynamic respData) {
+ LoaderBottomSheet.hideLoader();
+ Navigator.of(context).push(
+ FadePage(
+ page: DoctorProfilePage(),
+ ),
+ );
+ }, onError: (err) {
+ LoaderBottomSheet.hideLoader();
+ showCommonBottomSheetWithoutHeight(
+ context,
+ child: Utils.getErrorWidget(loadingText: err),
+ callBackFunc: () {},
+ isFullScreen: false,
+ isCloseButtonVisible: true,
+ );
+ });
+ }),
+ ),
+ ),
+ ),
+ );
+ },
+ separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h),
+ )
+ : SizedBox.shrink(),
+ SizedBox(height: 24.h),
+ ],
+ );
+ }),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ Container(
+ decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
+ color: AppColors.whiteColor,
+ borderRadius: 24.h,
+ hasShadow: true,
+ ),
+ child: CustomButton(
+ text: LocaleKeys.search.tr(context: context),
+ onPressed: () async {
+ textFocusNode.unfocus();
+ if (searchEditingController.text.isNotEmpty) {
+ bookAppointmentsViewModel.setIsDoctorSearchByNameStarted(true);
+ bookAppointmentsViewModel.setIsDoctorsListLoading(true);
+ // LoaderBottomSheet.showLoader();
+ await bookAppointmentsViewModel.getDoctorsList(
+ doctorName: searchEditingController.text,
+ onSuccess: (dynamic respData) {},
+ onError: (err) {
+ bookAppointmentsViewModel.setIsDoctorSearchByNameStarted(false);
+ showCommonBottomSheetWithoutHeight(
+ context,
+ child: Utils.getErrorWidget(loadingText: err),
+ callBackFunc: () {},
+ isFullScreen: false,
+ isCloseButtonVisible: true,
+ );
+ });
+ } else {
+ showCommonBottomSheetWithoutHeight(
+ context,
+ child: Utils.getErrorWidget(loadingText: "Please enter doctor name to search"),
+ callBackFunc: () {},
+ isFullScreen: false,
+ isCloseButtonVisible: true,
+ );
+ }
+ },
+ backgroundColor: AppColors.primaryRedColor,
+ borderColor: AppColors.primaryRedColor,
+ textColor: AppColors.whiteColor,
+ fontSize: 16,
+ fontWeight: FontWeight.w500,
+ borderRadius: 12,
+ padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
+ height: 50.h,
+ icon: AppAssets.search_icon,
+ iconColor: AppColors.whiteColor,
+ iconSize: 20.h,
+ ).paddingSymmetrical(24.h, 24.h),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/presentation/book_appointment/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart
index 496f7d5..214d4ad 100644
--- a/lib/presentation/book_appointment/select_clinic_page.dart
+++ b/lib/presentation/book_appointment/select_clinic_page.dart
@@ -1,8 +1,26 @@
+import 'dart:async';
+
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
+import 'package:hmg_patient_app_new/core/app_assets.dart';
+import 'package:hmg_patient_app_new/core/app_state.dart';
+import 'package:hmg_patient_app_new/core/dependencies.dart';
+import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
+import 'package:hmg_patient_app_new/core/utils/utils.dart';
+import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
+import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
+import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart';
+import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
+import 'package:hmg_patient_app_new/presentation/book_appointment/select_doctor_page.dart';
+import 'package:hmg_patient_app_new/presentation/book_appointment/select_livecare_clinic_page.dart';
+import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/clinic_card.dart';
import 'package:hmg_patient_app_new/presentation/lab/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
+import 'package:hmg_patient_app_new/widgets/input_widget.dart';
+import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart';
+import 'package:provider/provider.dart';
class SelectClinicPage extends StatefulWidget {
const SelectClinicPage({super.key});
@@ -12,18 +30,127 @@ class SelectClinicPage extends StatefulWidget {
}
class _SelectClinicPageState extends State {
+ TextEditingController searchEditingController = TextEditingController();
+ FocusNode textFocusNode = FocusNode();
+
+ late AppState appState;
+ late BookAppointmentsViewModel bookAppointmentsViewModel;
+
+ @override
+ void initState() {
+ scheduleMicrotask(() {
+ bookAppointmentsViewModel.getClinics();
+ });
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ textFocusNode.dispose();
+ super.dispose();
+ }
+
@override
Widget build(BuildContext context) {
+ bookAppointmentsViewModel = Provider.of(context, listen: false);
+ appState = getIt.get();
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
title: LocaleKeys.selectClinic.tr(context: context),
child: SingleChildScrollView(
- child: Column(
- children: [],
+ child: Padding(
+ padding: EdgeInsets.symmetric(horizontal: 24.h),
+ child: Consumer(builder: (context, bookAppointmentsVM, child) {
+ return Column(
+ children: [
+ SizedBox(height: 16.h),
+ TextInputWidget(
+ labelText: LocaleKeys.search.tr(context: context),
+ hintText: LocaleKeys.clinicName.tr(context: context),
+ controller: searchEditingController,
+ isEnable: true,
+ prefix: null,
+ autoFocus: false,
+ isBorderAllowed: false,
+ keyboardType: TextInputType.text,
+ focusNode: textFocusNode,
+ suffix: searchEditingController.text.isNotEmpty
+ ? GestureDetector(
+ onTap: () {
+ searchEditingController.clear();
+ bookAppointmentsViewModel.filterClinics("");
+ textFocusNode.unfocus();
+ },
+ child: Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown),
+ )
+ : null,
+ onChange: (value) {
+ bookAppointmentsViewModel.filterClinics(value!);
+ },
+ padding: EdgeInsets.symmetric(
+ vertical: ResponsiveExtension(10).h,
+ horizontal: ResponsiveExtension(15).h,
+ ),
+ ),
+ ListView.separated(
+ padding: EdgeInsets.only(top: 24.h),
+ shrinkWrap: true,
+ physics: NeverScrollableScrollPhysics(),
+ itemCount: bookAppointmentsVM.isClinicsListLoading ? 5 : bookAppointmentsVM.filteredClinicsList.length,
+ itemBuilder: (context, index) {
+ return bookAppointmentsVM.isClinicsListLoading
+ ? ClinicCard(
+ clinicsListResponseModel: GetClinicsListResponseModel(),
+ isLoading: bookAppointmentsVM.isClinicsListLoading,
+ )
+ : AnimationConfiguration.staggeredList(
+ position: index,
+ duration: const Duration(milliseconds: 500),
+ child: SlideAnimation(
+ verticalOffset: 100.0,
+ child: FadeInAnimation(
+ child: AnimatedContainer(
+ duration: Duration(milliseconds: 300),
+ curve: Curves.easeInOut,
+ decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true),
+ child: ClinicCard(
+ clinicsListResponseModel: bookAppointmentsVM.filteredClinicsList[index],
+ isLoading: bookAppointmentsVM.isClinicsListLoading,
+ ).onPress(() {
+ onClinicSelected(bookAppointmentsVM.filteredClinicsList[index]);
+ }),
+ ),
+ ),
+ ),
+ );
+ },
+ separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h),
+ ),
+ ],
+ );
+ }),
),
),
),
);
}
+
+ void onClinicSelected(GetClinicsListResponseModel clinic) {
+ bookAppointmentsViewModel.setSelectedClinic(clinic);
+ bookAppointmentsViewModel.setIsDoctorsListLoading(true);
+ if (clinic.isLiveCareClinicAndOnline ?? false) {
+ Navigator.of(context).push(
+ FadePage(
+ page: SelectLivecareClinicPage(),
+ ),
+ );
+ } else {
+ Navigator.of(context).push(
+ FadePage(
+ page: SelectDoctorPage(),
+ ),
+ );
+ }
+ }
}
diff --git a/lib/presentation/book_appointment/select_doctor_page.dart b/lib/presentation/book_appointment/select_doctor_page.dart
new file mode 100644
index 0000000..43449e4
--- /dev/null
+++ b/lib/presentation/book_appointment/select_doctor_page.dart
@@ -0,0 +1,158 @@
+import 'dart:async';
+
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
+import 'package:hmg_patient_app_new/core/app_assets.dart';
+import 'package:hmg_patient_app_new/core/app_state.dart';
+import 'package:hmg_patient_app_new/core/dependencies.dart';
+import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
+import 'package:hmg_patient_app_new/core/utils/utils.dart';
+import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
+import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
+import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart';
+import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart';
+import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
+import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_profile_page.dart';
+import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/doctor_card.dart';
+import 'package:hmg_patient_app_new/presentation/lab/collapsing_list_view.dart';
+import 'package:hmg_patient_app_new/theme/colors.dart';
+import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
+import 'package:hmg_patient_app_new/widgets/input_widget.dart';
+import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
+import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart';
+import 'package:provider/provider.dart';
+
+class SelectDoctorPage extends StatefulWidget {
+ SelectDoctorPage({super.key});
+
+ @override
+ State createState() => _SelectDoctorPageState();
+}
+
+class _SelectDoctorPageState extends State {
+ TextEditingController searchEditingController = TextEditingController();
+
+ FocusNode textFocusNode = FocusNode();
+
+ late AppState appState;
+ late BookAppointmentsViewModel bookAppointmentsViewModel;
+
+ @override
+ void initState() {
+ scheduleMicrotask(() {
+ bookAppointmentsViewModel.getDoctorsList();
+ });
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ bookAppointmentsViewModel = Provider.of(context, listen: false);
+ appState = getIt.get();
+ return Scaffold(
+ backgroundColor: AppColors.bgScaffoldColor,
+ body: CollapsingListView(
+ title: "Choose Doctor".needTranslation,
+ child: SingleChildScrollView(
+ child: Padding(
+ padding: EdgeInsets.symmetric(horizontal: 24.h),
+ child: Consumer(builder: (context, bookAppointmentsVM, child) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // TODO: Implement doctor filter functionality
+ SizedBox(height: 16.h),
+ TextInputWidget(
+ labelText: LocaleKeys.search.tr(context: context),
+ hintText: LocaleKeys.doctorName.tr(context: context),
+ controller: searchEditingController,
+ isEnable: true,
+ prefix: null,
+ autoFocus: false,
+ isBorderAllowed: false,
+ keyboardType: TextInputType.text,
+ focusNode: textFocusNode,
+ suffix: searchEditingController.text.isNotEmpty
+ ? GestureDetector(
+ onTap: () {
+ searchEditingController.clear();
+ bookAppointmentsViewModel.filterClinics("");
+ textFocusNode.unfocus();
+ },
+ child: Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown),
+ )
+ : null,
+ onChange: (value) {
+ bookAppointmentsViewModel.filterClinics(value!);
+ },
+ padding: EdgeInsets.symmetric(
+ vertical: ResponsiveExtension(10).h,
+ horizontal: ResponsiveExtension(15).h,
+ ),
+ ),
+ ListView.separated(
+ padding: EdgeInsets.only(top: 24.h),
+ shrinkWrap: true,
+ physics: NeverScrollableScrollPhysics(),
+ itemCount: bookAppointmentsVM.isDoctorsListLoading ? 5 : bookAppointmentsVM.doctorsList.length,
+ itemBuilder: (context, index) {
+ return bookAppointmentsVM.isDoctorsListLoading
+ ? DoctorCard(
+ doctorsListResponseModel: DoctorsListResponseModel(),
+ isLoading: true,
+ bookAppointmentsViewModel: bookAppointmentsViewModel,
+ )
+ : AnimationConfiguration.staggeredList(
+ position: index,
+ duration: const Duration(milliseconds: 500),
+ child: SlideAnimation(
+ verticalOffset: 100.0,
+ child: FadeInAnimation(
+ child: AnimatedContainer(
+ duration: Duration(milliseconds: 300),
+ curve: Curves.easeInOut,
+ decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true),
+ child: DoctorCard(
+ doctorsListResponseModel: bookAppointmentsVM.doctorsList[index],
+ isLoading: false,
+ bookAppointmentsViewModel: bookAppointmentsViewModel,
+ ).onPress(() async {
+ bookAppointmentsVM.setSelectedDoctor(bookAppointmentsVM.doctorsList[index]);
+ // bookAppointmentsVM.setSelectedDoctor(DoctorsListResponseModel());
+ LoaderBottomSheet.showLoader();
+ await bookAppointmentsVM.getDoctorProfile(onSuccess: (dynamic respData) {
+ LoaderBottomSheet.hideLoader();
+ Navigator.of(context).push(
+ FadePage(
+ page: DoctorProfilePage(),
+ ),
+ );
+ }, onError: (err) {
+ LoaderBottomSheet.hideLoader();
+ showCommonBottomSheetWithoutHeight(
+ context,
+ child: Utils.getErrorWidget(loadingText: err),
+ callBackFunc: () {},
+ isFullScreen: false,
+ isCloseButtonVisible: true,
+ );
+ });
+ }),
+ ),
+ ),
+ ),
+ );
+ },
+ separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h),
+ ),
+ SizedBox(height: 24.h),
+ ],
+ );
+ }),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/presentation/book_appointment/select_livecare_clinic_page.dart b/lib/presentation/book_appointment/select_livecare_clinic_page.dart
new file mode 100644
index 0000000..ef682c3
--- /dev/null
+++ b/lib/presentation/book_appointment/select_livecare_clinic_page.dart
@@ -0,0 +1,146 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:hmg_patient_app_new/core/app_assets.dart';
+import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
+import 'package:hmg_patient_app_new/core/utils/utils.dart';
+import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
+import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
+import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
+import 'package:hmg_patient_app_new/presentation/book_appointment/select_doctor_page.dart';
+import 'package:hmg_patient_app_new/presentation/lab/collapsing_list_view.dart';
+import 'package:hmg_patient_app_new/theme/colors.dart';
+import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
+import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart';
+
+class SelectLivecareClinicPage extends StatelessWidget {
+ const SelectLivecareClinicPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: AppColors.bgScaffoldColor,
+ body: Column(
+ children: [
+ Expanded(
+ child: CollapsingListView(
+ title: LocaleKeys.livecare.tr(context: context),
+ child: SingleChildScrollView(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SizedBox(height: 24.h),
+ LocaleKeys.livecareModalTop.tr(context: context).toText18(isBold: true),
+ SizedBox(height: 40.h),
+ Row(
+ children: [
+ Utils.buildSvgWithAssets(icon: AppAssets.immediate_service_icon, width: 58.h, height: 58.h),
+ SizedBox(width: 18.h),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ "Immediate service".needTranslation.toText18(color: AppColors.textColor, isBold: true),
+ "No need to wait, you will get medical consultation immediately via video call".needTranslation.toText14(color: AppColors.greyTextColor, weight: FontWeight.w500),
+ ],
+ ),
+ ),
+ ],
+ ),
+ SizedBox(height: 24.h),
+ Row(
+ children: [
+ Utils.buildSvgWithAssets(icon: AppAssets.no_visit_icon, width: 58.h, height: 58.h),
+ SizedBox(width: 18.h),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ "No visit required".needTranslation.toText18(color: AppColors.textColor, isBold: true),
+ LocaleKeys.livecarePoint5.tr(context: context).toText14(color: AppColors.greyTextColor, weight: FontWeight.w500),
+ ],
+ ),
+ ),
+ ],
+ ),
+ SizedBox(height: 24.h),
+ Row(
+ children: [
+ Utils.buildSvgWithAssets(icon: AppAssets.doctor_contact_icon, width: 58.h, height: 58.h),
+ SizedBox(width: 18.h),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ "Doctor will contact".needTranslation.toText18(color: AppColors.textColor, isBold: true),
+ "A specialised doctor will contact you and will be able to view your medical history".needTranslation.toText14(color: AppColors.greyTextColor, weight: FontWeight.w500),
+ ],
+ ),
+ ),
+ ],
+ ),
+ SizedBox(height: 24.h),
+ Row(
+ children: [
+ Utils.buildSvgWithAssets(icon: AppAssets.free_med_delivery_icon, width: 58.h, height: 58.h),
+ SizedBox(width: 18.h),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ "Free medicine delivery".needTranslation.toText18(color: AppColors.textColor, isBold: true),
+ "Offers free medicine delivery for the LiveCare appointment".needTranslation.toText14(color: AppColors.greyTextColor, weight: FontWeight.w500),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ],
+ ).paddingSymmetrical(24.h, 0.h),
+ ),
+ ),
+ ),
+ Column(
+ children: [
+ CustomButton(
+ text: "Yes please, I am in a hurry".needTranslation,
+ onPressed: () {},
+ backgroundColor: AppColors.primaryRedColor,
+ borderColor: AppColors.primaryRedColor,
+ textColor: AppColors.whiteColor,
+ fontSize: 16,
+ fontWeight: FontWeight.w500,
+ borderRadius: 12,
+ padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
+ height: 50.h,
+ icon: AppAssets.livecare_book_icon,
+ iconColor: AppColors.whiteColor,
+ iconSize: 18.h,
+ ).paddingSymmetrical(24.h, 0.h),
+ SizedBox(height: 16.h),
+ CustomButton(
+ text: "No, Thanks. I would like a physical visit".needTranslation,
+ onPressed: () {
+ Navigator.of(context).pop();
+ Navigator.of(context).push(
+ FadePage(
+ page: SelectDoctorPage(),
+ ),
+ );
+ },
+ backgroundColor: AppColors.secondaryLightRedColor,
+ borderColor: AppColors.secondaryLightRedColor,
+ textColor: AppColors.primaryRedColor,
+ fontSize: 16,
+ fontWeight: FontWeight.w500,
+ borderRadius: 12,
+ padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
+ height: 50.h,
+ ).paddingSymmetrical(24.h, 0.h),
+ SizedBox(height: 24.h),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/presentation/book_appointment/widgets/appointment_calendar.dart b/lib/presentation/book_appointment/widgets/appointment_calendar.dart
new file mode 100644
index 0000000..c880238
--- /dev/null
+++ b/lib/presentation/book_appointment/widgets/appointment_calendar.dart
@@ -0,0 +1,390 @@
+import 'dart:async';
+
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:hmg_patient_app_new/core/app_assets.dart';
+import 'package:hmg_patient_app_new/core/app_export.dart';
+import 'package:hmg_patient_app_new/core/app_state.dart';
+import 'package:hmg_patient_app_new/core/dependencies.dart';
+import 'package:hmg_patient_app_new/core/utils/date_util.dart';
+import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
+import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
+import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart';
+import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart';
+import 'package:hmg_patient_app_new/features/book_appointments/models/timeslots.dart';
+import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
+import 'package:hmg_patient_app_new/presentation/book_appointment/review_appointment_page.dart';
+import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart';
+import 'package:hmg_patient_app_new/theme/colors.dart';
+import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
+import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
+import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart';
+import 'package:lottie/lottie.dart';
+import 'package:provider/provider.dart';
+import 'package:smooth_corner/smooth_corner.dart';
+import 'package:syncfusion_flutter_calendar/calendar.dart';
+
+class AppointmentCalendar extends StatefulWidget {
+ const AppointmentCalendar({super.key});
+
+ @override
+ State createState() => _AppointmentCalendarState();
+}
+
+class _AppointmentCalendarState extends State {
+ late CalendarController _calendarController;
+
+ late AppState appState;
+ late BookAppointmentsViewModel bookAppointmentsViewModel;
+ late AuthenticationViewModel authVM;
+
+ var selectedDate = "";
+ var selectedDateDisplay = "";
+ var selectedNextDate = "";
+
+ int selectedButtonIndex = 0;
+ int selectedNextDayButtonIndex = -1;
+
+ List dayEvents = [];
+ List nextDayEvents = [];
+
+ late Map _events;
+
+ String selectedTime = "";
+
+ bool isWaitingAppointmentAvailable = false;
+ final _selectedDay = DateTime.now();
+
+ @override
+ void initState() {
+ scheduleMicrotask(() {
+ _calendarController = CalendarController();
+ _events = {
+ _selectedDay: ['Event A0']
+ };
+ _onDaySelected(DateUtil.convertStringToDate(bookAppointmentsViewModel.freeSlotsResponse[0]));
+ });
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ bookAppointmentsViewModel = Provider.of(context, listen: false);
+ authVM = Provider.of(context, listen: false);
+ appState = getIt.get();
+ return Padding(
+ padding: EdgeInsets.symmetric(horizontal: 0.h),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // SizedBox(height: 24.h),
+ // Row(
+ // crossAxisAlignment: CrossAxisAlignment.center,
+ // children: [
+ // "Pick a Date".toText20(weight: FontWeight.w600).expanded,
+ // Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, iconColor: Color(0xff2B353E)).onPress(() {
+ // Navigator.of(context).pop();
+ // }),
+ // ],
+ // ),
+ SizedBox(
+ height: 350.h,
+ child: SfCalendar(
+ controller: _calendarController,
+ minDate: DateTime.now(),
+ showNavigationArrow: true,
+ headerHeight: 60.h,
+ headerStyle: CalendarHeaderStyle(
+ backgroundColor: AppColors.scaffoldBgColor,
+ textAlign: TextAlign.start,
+ textStyle: TextStyle(fontSize: 18.fSize, fontWeight: FontWeight.w600, letterSpacing: -0.46, color: AppColors.primaryRedColor, fontFamily: "Poppins"),
+ ),
+ viewHeaderStyle: ViewHeaderStyle(
+ backgroundColor: AppColors.scaffoldBgColor,
+ dayTextStyle: TextStyle(fontSize: 14.fSize, fontWeight: FontWeight.w600, letterSpacing: -0.46, color: AppColors.textColor),
+ ),
+ view: CalendarView.month,
+ todayHighlightColor: Colors.transparent,
+ todayTextStyle: TextStyle(color: AppColors.textColor),
+ selectionDecoration: ShapeDecoration(
+ color: AppColors.transparent,
+ shape: SmoothRectangleBorder(
+ borderRadius: BorderRadius.circular(12 ?? 0),
+ smoothness: 1,
+ side: BorderSide(color: AppColors.primaryRedColor, width: 1.5),
+ ),
+ ),
+ cellBorderColor: AppColors.transparent,
+ dataSource: MeetingDataSource(_getDataSource()),
+ monthCellBuilder: (context, details) => Padding(
+ padding: EdgeInsets.all(12.h),
+ child: details.date.day.toString().toText14(
+ isCenter: true,
+ color: details.date == _calendarController.selectedDate ? AppColors.primaryRedColor : AppColors.textColor,
+ ),
+ ),
+ monthViewSettings: MonthViewSettings(
+ dayFormat: "EEE",
+ appointmentDisplayMode: MonthAppointmentDisplayMode.indicator,
+ showTrailingAndLeadingDates: false,
+ appointmentDisplayCount: 1,
+ monthCellStyle: MonthCellStyle(
+ textStyle: TextStyle(fontSize: 19.fSize),
+ ),
+ ),
+ onTap: (CalendarTapDetails details) {
+ _calendarController.selectedDate = details.date;
+ _onDaySelected(details.date!);
+ },
+ ),
+ ),
+ Transform.translate(
+ offset: const Offset(0.0, -20.0),
+ child: selectedDateDisplay.toText16(weight: FontWeight.w500),
+ ),
+ //TODO: Add Next Day Span here
+ SizedBox(
+ height: 100.h,
+ child: SingleChildScrollView(
+ scrollDirection: Axis.vertical,
+ child: Wrap(
+ direction: Axis.horizontal,
+ alignment: WrapAlignment.start,
+ spacing: 8.h,
+ runSpacing: 8.h,
+ children: List.generate(
+ dayEvents.length, // Generate a large number of items to ensure scrolling
+ (index) => TimeSlotChip(
+ label: dayEvents[index].isoTime!,
+ isSelected: index == selectedButtonIndex,
+ onTap: () {
+ setState(() {
+ selectedButtonIndex = index;
+ selectedTime = dayEvents[index].isoTime!;
+ });
+ },
+ ),
+ ),
+ ),
+ ),
+ ),
+ SizedBox(height: 16.h),
+ CustomButton(
+ text: "Select".needTranslation,
+ onPressed: () async {
+ if (appState.isAuthenticated) {
+ bookAppointmentsViewModel.setSelectedAppointmentDateTime(selectedDate, selectedTime);
+ Navigator.of(context).pop();
+ Navigator.of(context).push(
+ FadePage(
+ page: ReviewAppointmentPage(),
+ ),
+ );
+ } else {
+ showCommonBottomSheetWithoutHeight(
+ context,
+ title: LocaleKeys.notice.tr(context: context),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Lottie.asset(AppAnimations.errorAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
+ SizedBox(height: 8.h),
+ (LocaleKeys.loginToUseService.tr()).toText16(color: AppColors.blackColor),
+ SizedBox(height: 16.h),
+ Row(
+ children: [
+ Expanded(
+ child: CustomButton(
+ text: LocaleKeys.cancel.tr(),
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ backgroundColor: AppColors.secondaryLightRedColor,
+ borderColor: AppColors.secondaryLightRedColor,
+ textColor: AppColors.primaryRedColor,
+ icon: AppAssets.cancel,
+ iconColor: AppColors.primaryRedColor,
+ ),
+ ),
+ SizedBox(width: 8.h),
+ Expanded(
+ child: CustomButton(
+ text: LocaleKeys.confirm.tr(),
+ onPressed: () async {
+ Navigator.of(context).pop();
+ Navigator.pushAndRemoveUntil(
+ context,
+ FadePage(
+ page: LandingNavigation(),
+ ),
+ (r) => false);
+ await authVM.onLoginPressed();
+ // Navigator.of(context).push(
+ // FadePage(page: MyAppointmentsPage()),
+ // );
+ },
+ backgroundColor: AppColors.bgGreenColor,
+ borderColor: AppColors.bgGreenColor,
+ textColor: Colors.white,
+ icon: AppAssets.confirm,
+ ),
+ ),
+ ],
+ ),
+ SizedBox(height: 16.h),
+ ],
+ ).center,
+ callBackFunc: () {},
+ isFullScreen: false,
+ isCloseButtonVisible: true,
+ );
+ }
+ },
+ backgroundColor: AppColors.primaryRedColor,
+ borderColor: AppColors.primaryRedColor,
+ textColor: AppColors.whiteColor,
+ fontSize: 16,
+ fontWeight: FontWeight.w500,
+ borderRadius: 12,
+ padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
+ height: 50.h,
+ ),
+ ],
+ ),
+ );
+ }
+
+ List _getDataSource() {
+ final List meetings = [];
+ for (var slot in bookAppointmentsViewModel.freeSlotsResponse) {
+ final startTime = DateUtil.convertStringToDate(slot);
+
+ final endTime = startTime.add(const Duration(minutes: 15));
+
+ meetings.add(Meeting(
+ "Available", // Or leave empty with ""
+ startTime,
+ endTime,
+ AppColors.primaryRedColor,
+ false,
+ "" // Optional notes
+ ));
+ }
+ return meetings;
+ }
+
+ // TODO:
+ openTimeSlotsPickerForDate(DateTime dateStart, List freeSlots) {
+ dayEvents.clear();
+ DateTime dateStartObj = new DateTime(dateStart.year, dateStart.month, dateStart.day, 0, 0, 0, 0, 0);
+ if (isWaitingAppointmentAvailable && DateUtils.isSameDay(dateStart, DateTime.now())) {
+ dayEvents.add(TimeSlot(isoTime: "Waiting Appointment", start: DateTime.now(), end: DateTime.now(), vidaDate: ""));
+ }
+ freeSlots.forEach((v) {
+ if (v.start == dateStartObj) dayEvents.add(v);
+ });
+ selectedButtonIndex = 0;
+ List