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> timeList = []; + for (var i = 0; i < dayEvents.length; i++) { + Map timeSlot = {"isoTime": dayEvents[i].isoTime, "start": dayEvents[i].start.toString(), "end": dayEvents[i].end.toString(), "vidaDate": dayEvents[i].vidaDate}; + timeList.add(timeSlot); + } + selectedTime = dayEvents[selectedButtonIndex].isoTime!; + } + + void _onDaySelected(DateTime day) { + final DateFormat formatter = DateFormat('yyyy-MM-dd'); + setState(() { + selectedDateDisplay = DateUtil.getMonthDayYearDateFormatted(day); + selectedNextDate = DateUtil.getWeekDayMonthDayYearDateFormatted(day.add(Duration(days: 1)), "en"); + _calendarController.selectedDate = day; + openTimeSlotsPickerForDate(day, bookAppointmentsViewModel.docFreeSlots); + selectedDate = formatter.format(day); + selectedNextDayButtonIndex = -1; + print(_calendarController.selectedDate); + }); + } +} + +class TimeSlotChip extends StatelessWidget { + final String label; + final bool isSelected; + final VoidCallback? onTap; + + const TimeSlotChip({super.key, required this.label, this.isSelected = false, this.onTap}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 18.h, vertical: 8.h), + decoration: ShapeDecoration( + color: AppColors.whiteColor, + shape: SmoothRectangleBorder( + borderRadius: BorderRadius.circular(8.h), + smoothness: 1, + side: BorderSide(color: isSelected ? AppColors.primaryRedColor : AppColors.borderOnlyColor.withOpacity(0.2), width: 1), + ), + ), + child: label.toText12( + color: isSelected ? AppColors.primaryRedColor : Colors.black87, + fontWeight: FontWeight.w500, + ), + ), + ); + } +} + +class MeetingDataSource extends CalendarDataSource { + MeetingDataSource(List source) { + appointments = source; + } + + @override + DateTime getStartTime(int index) { + return _getMeetingData(index)!.from; + } + + @override + DateTime getEndTime(int index) { + return _getMeetingData(index)!.to; + } + + @override + String getSubject(int index) { + return _getMeetingData(index)!.eventName; + } + + @override + Color getColor(int index) { + return _getMeetingData(index)!.background; + } + + @override + bool isAllDay(int index) { + return _getMeetingData(index)!.isAllDay; + } + + Meeting? _getMeetingData(int index) { + final dynamic meeting = appointments?[index]; + Meeting? meetingData; + if (meeting is Meeting) { + meetingData = meeting; + } + return meetingData; + } +} + +class Meeting { + Meeting(this.eventName, this.from, this.to, this.background, this.isAllDay, this.notes); + + String eventName; + DateTime from; + DateTime to; + Color background; + bool isAllDay; + String notes; +} diff --git a/lib/presentation/book_appointment/widgets/clinic_card.dart b/lib/presentation/book_appointment/widgets/clinic_card.dart new file mode 100644 index 0000000..5cfc624 --- /dev/null +++ b/lib/presentation/book_appointment/widgets/clinic_card.dart @@ -0,0 +1,42 @@ +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/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class ClinicCard extends StatelessWidget { + ClinicCard({super.key, required this.clinicsListResponseModel, required this.isLoading}); + + GetClinicsListResponseModel clinicsListResponseModel; + bool isLoading; + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: Column( + children: [ + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + "".toText16(isBold: true).toShimmer2(isShow: isLoading), + (clinicsListResponseModel.isLiveCareClinicAndOnline ?? true) + ? Utils.buildSvgWithAssets(icon: AppAssets.livecare_clinic_icon, width: 32.h, height: 32.h, fit: BoxFit.contain).toShimmer2(isShow: isLoading) + : SizedBox.shrink(), + ]), + SizedBox(height: 16.h), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Expanded(child: (isLoading ? "Cardiology" : clinicsListResponseModel.clinicDescription!).toText16(isBold: true).toShimmer2(isShow: isLoading)), + Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, width: 15.h, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor).toShimmer2(isShow: isLoading), + ]), + ], + ), + ); + } +} diff --git a/lib/presentation/book_appointment/widgets/doctor_card.dart b/lib/presentation/book_appointment/widgets/doctor_card.dart new file mode 100644 index 0000000..1e1e0ae --- /dev/null +++ b/lib/presentation/book_appointment/widgets/doctor_card.dart @@ -0,0 +1,152 @@ +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/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/widgets/appointment_calendar.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'; + +class DoctorCard extends StatelessWidget { + DoctorCard({super.key, required this.doctorsListResponseModel, required this.isLoading, required this.bookAppointmentsViewModel}); + + DoctorsListResponseModel doctorsListResponseModel; + bool isLoading = false; + BookAppointmentsViewModel bookAppointmentsViewModel; + + @override + Widget build(BuildContext context) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(14.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Image.network( + isLoading + ? "https://hmgwebservices.com/Images/MobileImages/OALAY/1439.png" + : doctorsListResponseModel.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/OALAY/1439.png", + width: 63.h, + height: 63.h, + fit: BoxFit.fill, + ).circle(100).toShimmer2(isShow: isLoading), + SizedBox(width: 8.h), + Expanded( + flex: 9, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.49, + child: (isLoading ? "Dr John Smith" : "${doctorsListResponseModel.doctorTitle} ${doctorsListResponseModel.name}").toString().toText16(isBold: true, maxlines: 1), + ).toShimmer2(isShow: isLoading), + Image.network( + isLoading ? "https://hmgwebservices.com/Images/flag/SYR.png" : doctorsListResponseModel.nationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SYR.png", + width: 20.h, + height: 15.h, + fit: BoxFit.fill, + ).toShimmer2(isShow: isLoading), + ], + ), + SizedBox(height: 2.h), + (isLoading + ? "Consultant Cardiologist" + : doctorsListResponseModel.speciality!.isNotEmpty + ? doctorsListResponseModel.speciality!.first + : "") + .toString() + .toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor, maxLine: 1) + .toShimmer2(isShow: isLoading), + ], + ), + ), + Expanded( + flex: 1, + child: Utils.buildSvgWithAssets(icon: AppAssets.doctor_profile_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown).toShimmer2(isShow: isLoading), + ), + ], + ), + SizedBox(height: 12.h), + Wrap( + direction: Axis.horizontal, + spacing: 3.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget( + labelText: "Clinic: ${isLoading ? "Cardiologist" : doctorsListResponseModel.clinicName}".needTranslation, + ).toShimmer2(isShow: isLoading), + AppCustomChipWidget( + labelText: "Branch: ${isLoading ? "Olaya Hospital" : doctorsListResponseModel.projectName}".needTranslation, + ).toShimmer2(isShow: isLoading), + AppCustomChipWidget( + icon: AppAssets.rating_icon, + iconColor: AppColors.ratingColorYellow, + labelText: "Rating: ${isLoading ? 4.78 : doctorsListResponseModel.decimalDoctorRate}".needTranslation, + ).toShimmer2(isShow: isLoading), + ], + ), + SizedBox(height: 12.h), + CustomButton( + text: LocaleKeys.bookAppo.tr(context: context), + onPressed: () async { + bookAppointmentsViewModel.setSelectedDoctor(doctorsListResponseModel); + 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: Color(0xffFEE9EA), + borderColor: Color(0xffFEE9EA), + textColor: Color(0xffED1C2B), + fontSize: 14, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + icon: AppAssets.add_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 15.h, + ).toShimmer2(isShow: isLoading), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/habib_wallet/recharge_wallet_page.dart b/lib/presentation/habib_wallet/recharge_wallet_page.dart index ff1a505..02a9426 100644 --- a/lib/presentation/habib_wallet/recharge_wallet_page.dart +++ b/lib/presentation/habib_wallet/recharge_wallet_page.dart @@ -1,4 +1,5 @@ import 'package:easy_localization/easy_localization.dart'; +import 'package:family_bottom_sheet/family_bottom_sheet.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'; @@ -122,9 +123,27 @@ class _RechargeWalletPageState extends State { ), Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h), ], - ).onPress(() { + ).onPress(() async { // showCommonBottomSheetWithoutHeight(context, title: "Select Medical File".needTranslation, child: SelectMedicalFile(), callBackFunc: () {}, isFullScreen: false); - showCommonBottomSheetWithoutHeight(context, title: "Select Medical File".needTranslation, child: const MultiPageBottomSheet(), callBackFunc: () {}, isFullScreen: false); + // showCommonBottomSheetWithoutHeight(context, title: "Select Medical File".needTranslation, child: const MultiPageBottomSheet(), callBackFunc: () {}, isFullScreen: false); + await FamilyModalSheet.show( + context: context, + contentBackgroundColor: AppColors.scaffoldBgColor, + backgroundColor: AppColors.bottomSheetBgColor, + mainContentPadding: EdgeInsets.all(24.h), + isScrollControlled: true, + safeAreaMinimum: EdgeInsets.zero, + useSafeArea: false, + sheetAnimationStyle: AnimationStyle( + duration: Duration(milliseconds: 500), // Custom animation duration + reverseDuration: Duration(milliseconds: 300), // Custom reverse animation duration + ), + builder: (ctx) { + return const MultiPageBottomSheet(); + }, + + // Optional configurations + ); }), SizedBox(height: 16.h), Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h), diff --git a/lib/presentation/habib_wallet/widgets/select-medical_file.dart b/lib/presentation/habib_wallet/widgets/select-medical_file.dart index a70edb3..4724ea1 100644 --- a/lib/presentation/habib_wallet/widgets/select-medical_file.dart +++ b/lib/presentation/habib_wallet/widgets/select-medical_file.dart @@ -38,199 +38,89 @@ class MultiPageBottomSheet extends StatefulWidget { } class _MultiPageBottomSheetState extends State { - final PageController _pageController = PageController(); - int _currentPage = 0; - final int _totalPages = 3; - late AppState appState; - @override - void dispose() { - _pageController.dispose(); - super.dispose(); - } - - void _nextPage() { - if (_currentPage < _totalPages - 1) { - _pageController.animateToPage( - _currentPage + 2, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - } - } - - void _previousPage() { - if (_currentPage > 0) { - _pageController.previousPage( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - } - } - @override Widget build(BuildContext context) { appState = getIt.get(); - return Container( + return SizedBox( height: MediaQuery.of(context).size.height * 0.38, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 12.h), - Expanded( - child: PageView( - controller: _pageController, - onPageChanged: (index) { - setState(() { - _currentPage = index; - }); - }, - children: [ - _buildPage1(), - _buildPage2(), - _buildPage3(), - ], - ), - ), - ], - ), - ); - } - - Widget _buildPage1() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 16.h, - hasShadow: false, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LocaleKeys.myMedicalFile.tr(context: context).toText16(color: AppColors.textColor, weight: FontWeight.w500), - "${LocaleKeys.fileno.tr(context: context)}: ${appState.getAuthenticatedUser()!.patientId}".toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), - ], - ), - Utils.buildSvgWithAssets(icon: AppAssets.forward_chevron_icon, iconColor: AppColors.textColor, width: 15.h, height: 15.h), + "Select Medical File".toText20(weight: FontWeight.w600).expanded, + Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, iconColor: Color(0xff2B353E)).onPress(() { + Navigator.of(context).pop(); + }), ], - ).paddingAll(16.h), - ).onPress(() { - Navigator.of(context).pop(); - }), - SizedBox(height: 16.h), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 16.h, - hasShadow: false, ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LocaleKeys.familyTitle.tr(context: context).toText16(color: AppColors.textColor, weight: FontWeight.w500), - "Select a medical file from your family".needTranslation.toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), - ], - ), - Utils.buildSvgWithAssets(icon: AppAssets.forward_chevron_icon, iconColor: AppColors.textColor, width: 15.h, height: 15.h), - ], - ).paddingAll(16.h), - ), - SizedBox(height: 16.h), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 16.h, - hasShadow: false, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LocaleKeys.otherAccount.tr(context: context).toText16(color: AppColors.textColor, weight: FontWeight.w500), - "Any active medical file from HMG".toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), - ], - ), - Utils.buildSvgWithAssets(icon: AppAssets.forward_chevron_icon, iconColor: AppColors.textColor, width: 15.h, height: 15.h), - ], - ).paddingAll(16.h), - ).onPress(() { - _pageController.animateToPage( - 2, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - }), - ], - ); - } - - Widget _buildPage2() { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Page 2: Settings', - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 16), - SwitchListTile( - title: const Text('Enable notifications'), - value: true, - onChanged: (value) {}, - ), - SwitchListTile( - title: const Text('Dark mode'), - value: false, - onChanged: (value) {}, - ), - const ListTile( - leading: Icon(Icons.language), - title: Text('Language'), - subtitle: Text('English'), - trailing: Icon(Icons.arrow_forward_ios), - ), - ], - ), - ); - } - - Widget _buildPage3() { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Page 3: Summary', - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 16), - const Card( - child: Padding( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Configuration Complete!'), - SizedBox(height: 8), - Text('Your settings have been saved successfully.'), - ], - ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.h, + hasShadow: false, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.myMedicalFile.tr(context: context).toText16(color: AppColors.textColor, weight: FontWeight.w500), + "${LocaleKeys.fileno.tr(context: context)}: ${appState.getAuthenticatedUser()!.patientId}".toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), + ], + ), + Utils.buildSvgWithAssets(icon: AppAssets.forward_chevron_icon, iconColor: AppColors.textColor, width: 15.h, height: 15.h), + ], + ).paddingAll(16.h), + ).onPress(() { + Navigator.of(context).pop(); + }), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.h, + hasShadow: false, ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.familyTitle.tr(context: context).toText16(color: AppColors.textColor, weight: FontWeight.w500), + "Select a medical file from your family".needTranslation.toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), + ], + ), + Utils.buildSvgWithAssets(icon: AppAssets.forward_chevron_icon, iconColor: AppColors.textColor, width: 15.h, height: 15.h), + ], + ).paddingAll(16.h), ), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.h, + hasShadow: false, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.otherAccount.tr(context: context).toText16(color: AppColors.textColor, weight: FontWeight.w500), + "Any active medical file from HMG".toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), + ], + ), + Utils.buildSvgWithAssets(icon: AppAssets.forward_chevron_icon, iconColor: AppColors.textColor, width: 15.h, height: 15.h), + ], + ).paddingAll(16.h), + ).onPress(() {}), ], ), ); diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 26e5147..8831fc6 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -17,12 +17,14 @@ import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_vie import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/authentication/quick_login.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; import 'package:hmg_patient_app_new/presentation/home/data/landing_page_data.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/habib_wallet_card.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/large_service_card.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/small_service_card.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/welcome_widget.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; +import 'package:hmg_patient_app_new/presentation/profile_settings/profile_settings.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'; @@ -89,24 +91,32 @@ class _LandingPageState extends State { children: [ appState.isAuthenticated ? WelcomeWidget( - onTap: () {}, - name: ('${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!}'), - imageUrl: appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.male_img : AppAssets.femaleImg, - ) - : CustomButton( - text: LocaleKeys.loginOrRegister.tr(context: context), - onPressed: () async { - await authVM.onLoginPressed(); - }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), - fontSize: 16, - fontWeight: FontWeight.w500, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 50, + onTap: () { + Navigator.of(context).push( + FadePage( + page: ProfileSettings(), ), + ); + }, + name: ('${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!}'), + imageUrl: appState + .getAuthenticatedUser() + ?.gender == 1 ? AppAssets.male_img : AppAssets.femaleImg, + ) + : CustomButton( + text: LocaleKeys.loginOrRegister.tr(context: context), + onPressed: () async { + await authVM.onLoginPressed(); + }, + backgroundColor: Color(0xffFEE9EA), + borderColor: Color(0xffFEE9EA), + textColor: Color(0xffED1C2B), + fontSize: 16, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 50, + ), Row( children: [ Utils.buildSvgWithAssets(icon: AppAssets.bell, height: 20, width: 20).onPress(() { @@ -141,153 +151,156 @@ class _LandingPageState extends State { SizedBox(height: 16.h), appState.isAuthenticated ? Column( - children: [ - Container( - width: double.infinity, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24, - ), - child: Padding( - padding: EdgeInsets.all(12.h), - child: Column( - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon, width: 32.h, height: 32.h), - SizedBox(height: 12.h), - "You do not have any upcoming appointment. Please book an appointment".toText12(isCenter: true), - SizedBox(height: 12.h), - CustomButton( - text: LocaleKeys.bookAppo.tr(context: context), - onPressed: () { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (BuildContext context) => LandingPage()), - ); - }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), - fontSize: 14, - fontWeight: FontWeight.w500, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 40, - icon: AppAssets.add_icon, - iconColor: AppColors.primaryRedColor, + children: [ + Container( + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24, + ), + child: Padding( + padding: EdgeInsets.all(12.h), + child: Column( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon, width: 32.h, height: 32.h), + SizedBox(height: 12.h), + "You do not have any upcoming appointment. Please book an appointment".toText12(isCenter: true), + SizedBox(height: 12.h), + CustomButton( + text: LocaleKeys.bookAppo.tr(context: context), + onPressed: () { + Navigator.of(context) + .push( + FadePage( + page: BookAppointmentPage(), ), - ], - ), - ), - ).paddingSymmetrical(24.h, 0.h), - SizedBox(height: 12.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - "Quick Links".toText16(isBold: true), - Row( - children: [ - "View medical file".toText12(color: AppColors.primaryRedColor), - SizedBox(width: 2.h), - Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h), - ], - ), - ], - ).paddingSymmetrical(24.h, 0.h).onPress(() { - Navigator.of(context).push( - FadePage( - page: MedicalFilePage(), - ), - ); - }), - SizedBox(height: 12.h), - Container( - height: 127.h, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24, + ); + }, + backgroundColor: Color(0xffFEE9EA), + borderColor: Color(0xffFEE9EA), + textColor: Color(0xffED1C2B), + fontSize: 14, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40, + icon: AppAssets.add_icon, + iconColor: AppColors.primaryRedColor, ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Column( - children: [ - Expanded( - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: LandingPageData.getLoggedInServiceCardsList.length, - shrinkWrap: true, - padding: const EdgeInsets.only(left: 0, right: 8), - itemBuilder: (context, index) { - return AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 1000), - child: SlideAnimation( - horizontalOffset: 100.0, - child: FadeInAnimation( - child: SmallServiceCard( - icon: LandingPageData.getLoggedInServiceCardsList[index].icon, - title: LandingPageData.getLoggedInServiceCardsList[index].title, - subtitle: LandingPageData.getLoggedInServiceCardsList[index].subtitle, - iconColor: LandingPageData.getLoggedInServiceCardsList[index].iconColor, - textColor: LandingPageData.getLoggedInServiceCardsList[index].textColor, - backgroundColor: LandingPageData.getLoggedInServiceCardsList[index].backgroundColor, - isBold: LandingPageData.getLoggedInServiceCardsList[index].isBold, - serviceName: LandingPageData.getLoggedInServiceCardsList[index].serviceName, - ), - ), - ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => 0.width, + ], + ), + ), + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 12.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Quick Links".toText16(isBold: true), + Row( + children: [ + "View medical file".toText12(color: AppColors.primaryRedColor), + SizedBox(width: 2.h), + Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h), + ], + ), + ], + ).paddingSymmetrical(24.h, 0.h).onPress(() { + Navigator.of(context).push( + FadePage( + page: MedicalFilePage(), + ), + ); + }), + SizedBox(height: 12.h), + Container( + height: 127.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + children: [ + Expanded( + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: LandingPageData.getLoggedInServiceCardsList.length, + shrinkWrap: true, + padding: const EdgeInsets.only(left: 0, right: 8), + itemBuilder: (context, index) { + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 1000), + child: SlideAnimation( + horizontalOffset: 100.0, + child: FadeInAnimation( + child: SmallServiceCard( + icon: LandingPageData.getLoggedInServiceCardsList[index].icon, + title: LandingPageData.getLoggedInServiceCardsList[index].title, + subtitle: LandingPageData.getLoggedInServiceCardsList[index].subtitle, + iconColor: LandingPageData.getLoggedInServiceCardsList[index].iconColor, + textColor: LandingPageData.getLoggedInServiceCardsList[index].textColor, + backgroundColor: LandingPageData.getLoggedInServiceCardsList[index].backgroundColor, + isBold: LandingPageData.getLoggedInServiceCardsList[index].isBold, + serviceName: LandingPageData.getLoggedInServiceCardsList[index].serviceName, + ), + ), ), - ), - ], + ); + }, + separatorBuilder: (BuildContext cxt, int index) => 0.width, ), ), - ).paddingSymmetrical(24.h, 0.h), - ], - ) - : Container( - height: 127.h, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24, + ], ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Column( - children: [ - Expanded( - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: LandingPageData.getNotLoggedInServiceCardsList.length, - shrinkWrap: true, - padding: const EdgeInsets.only(left: 0, right: 8), - itemBuilder: (context, index) { - return AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 1000), - child: SlideAnimation( - horizontalOffset: 100.0, - child: FadeInAnimation( - child: SmallServiceCard( - icon: LandingPageData.getNotLoggedInServiceCardsList[index].icon, - title: LandingPageData.getNotLoggedInServiceCardsList[index].title, - subtitle: LandingPageData.getNotLoggedInServiceCardsList[index].subtitle, - iconColor: LandingPageData.getNotLoggedInServiceCardsList[index].iconColor, - textColor: LandingPageData.getNotLoggedInServiceCardsList[index].textColor, - backgroundColor: LandingPageData.getNotLoggedInServiceCardsList[index].backgroundColor, - isBold: LandingPageData.getNotLoggedInServiceCardsList[index].isBold, - ), - ), - ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => 0.width, + ), + ).paddingSymmetrical(24.h, 0.h), + ], + ) + : Container( + height: 127.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + children: [ + Expanded( + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: LandingPageData.getNotLoggedInServiceCardsList.length, + shrinkWrap: true, + padding: const EdgeInsets.only(left: 0, right: 8), + itemBuilder: (context, index) { + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 1000), + child: SlideAnimation( + horizontalOffset: 100.0, + child: FadeInAnimation( + child: SmallServiceCard( + icon: LandingPageData.getNotLoggedInServiceCardsList[index].icon, + title: LandingPageData.getNotLoggedInServiceCardsList[index].title, + subtitle: LandingPageData.getNotLoggedInServiceCardsList[index].subtitle, + iconColor: LandingPageData.getNotLoggedInServiceCardsList[index].iconColor, + textColor: LandingPageData.getNotLoggedInServiceCardsList[index].textColor, + backgroundColor: LandingPageData.getNotLoggedInServiceCardsList[index].backgroundColor, + isBold: LandingPageData.getNotLoggedInServiceCardsList[index].isBold, + ), + ), ), - ), - ], + ); + }, + separatorBuilder: (BuildContext cxt, int index) => 0.width, ), ), - ).paddingSymmetrical(24.h, 0.h), + ], + ), + ), + ).paddingSymmetrical(24.h, 0.h), SizedBox(height: 16.h), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/presentation/home/navigation_screen.dart b/lib/presentation/home/navigation_screen.dart index bdce393..8566aa8 100644 --- a/lib/presentation/home/navigation_screen.dart +++ b/lib/presentation/home/navigation_screen.dart @@ -24,7 +24,7 @@ class _LandingNavigationState extends State { children: [ const LandingPage(), MedicalFilePage(), - const BookAppointmentPage(), + BookAppointmentPage(), const LandingPage(), const LandingPage(), ], diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 48193e8..871a97c 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -11,6 +11,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/insurance/insurance_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/patient_sickleave_response_model.dart'; @@ -20,6 +22,7 @@ import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_mo import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart'; import 'package:hmg_patient_app_new/presentation/appointments/my_doctors_page.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart'; import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart'; import 'package:hmg_patient_app_new/presentation/insurance/widgets/patient_insurance_card.dart'; import 'package:hmg_patient_app_new/presentation/lab/collapsing_list_view.dart'; @@ -33,8 +36,10 @@ import 'package:hmg_patient_app_new/presentation/prescriptions/prescriptions_lis 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/custom_tab_bar.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/shimmer/movies_shimmer_widget.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; @@ -54,15 +59,18 @@ class _MedicalFilePageState extends State { late AppState appState; late MyAppointmentsViewModel myAppointmentsViewModel; late MedicalFileViewModel medicalFileViewModel; + late BookAppointmentsViewModel bookAppointmentsViewModel; int currentIndex = 0; @override void initState() { scheduleMicrotask(() { - insuranceViewModel.initInsuranceProvider(); - medicalFileViewModel.setIsPatientSickLeaveListLoading(true); - medicalFileViewModel.getPatientSickLeaveList(); + if (appState.isAuthenticated) { + insuranceViewModel.initInsuranceProvider(); + medicalFileViewModel.setIsPatientSickLeaveListLoading(true); + medicalFileViewModel.getPatientSickLeaveList(); + } }); super.initState(); } @@ -72,6 +80,7 @@ class _MedicalFilePageState extends State { insuranceViewModel = Provider.of(context, listen: false); myAppointmentsViewModel = Provider.of(context, listen: false); medicalFileViewModel = Provider.of(context, listen: false); + bookAppointmentsViewModel = Provider.of(context, listen: false); appState = getIt.get(); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, @@ -243,6 +252,46 @@ class _MedicalFilePageState extends State { ); } + openDoctorScheduleCalendar(PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel) async { + DoctorsListResponseModel doctor = DoctorsListResponseModel( + clinicID: patientAppointmentHistoryResponseModel.clinicID, + projectID: patientAppointmentHistoryResponseModel.projectID, + doctorID: patientAppointmentHistoryResponseModel.doctorID, + doctorImageURL: patientAppointmentHistoryResponseModel.doctorImageURL, + doctorTitle: patientAppointmentHistoryResponseModel.doctorTitle, + name: patientAppointmentHistoryResponseModel.doctorNameObj, + nationalityFlagURL: "https://hmgwebservices.com/Images/flag/SYR.png", + speciality: [], + clinicName: patientAppointmentHistoryResponseModel.clinicName, + projectName: 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, + ); + }); + } + Widget getSelectedTabData(int index) { switch (index) { case 0: @@ -291,10 +340,16 @@ class _MedicalFilePageState extends State { ? MedicalFileAppointmentCard( patientAppointmentHistoryResponseModel: PatientAppointmentHistoryResponseModel(), myAppointmentsViewModel: myAppointmentsVM, + onRescheduleTap: () {}, + onAskDoctorTap: () {}, ) : MedicalFileAppointmentCard( patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index], myAppointmentsViewModel: myAppointmentsViewModel, + onRescheduleTap: () { + openDoctorScheduleCalendar(myAppointmentsVM.patientAppointmentsHistoryList[index]); + }, + onAskDoctorTap: () {}, ), ), ), diff --git a/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart b/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart index 48cc328..6d13ee7 100644 --- a/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart +++ b/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart @@ -16,11 +16,14 @@ import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; class MedicalFileAppointmentCard extends StatelessWidget { - MedicalFileAppointmentCard({super.key, required this.patientAppointmentHistoryResponseModel, required this.myAppointmentsViewModel}); + MedicalFileAppointmentCard({super.key, required this.patientAppointmentHistoryResponseModel, required this.myAppointmentsViewModel, required this.onRescheduleTap, required this.onAskDoctorTap}); PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel; MyAppointmentsViewModel myAppointmentsViewModel; + late Function onRescheduleTap; + late Function onAskDoctorTap; + @override Widget build(BuildContext context) { return Column( @@ -164,7 +167,9 @@ class MedicalFileAppointmentCard extends StatelessWidget { ) : CustomButton( text: "Rebook".needTranslation, - onPressed: () {}, + onPressed: () { + onRescheduleTap(); + }, backgroundColor: AppColors.greyColor, borderColor: AppColors.greyColor, textColor: AppColors.blackColor, diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index 66c3877..8ec59f9 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -40,6 +40,7 @@ class TextInputWidget extends StatelessWidget { final SelectionTypeEnum? selectionType; final num? fontSize; final bool? isWalletAmountInput; + final Widget? suffix; // final List countryList; // final Function(Country)? onCountryChange; @@ -69,6 +70,7 @@ class TextInputWidget extends StatelessWidget { this.selectionType, this.fontSize = 14, this.isWalletAmountInput = false, + this.suffix, // this.countryList = const [], // this.onCountryChange, }); @@ -231,24 +233,34 @@ class TextInputWidget extends StatelessWidget { autofocus: autoFocus, textInputAction: TextInputAction.done, cursorHeight: isWalletAmountInput! ? 40.h : 18.h, + onTapOutside: (event) { + FocusManager.instance.primaryFocus?.unfocus(); + }, style: TextStyle(fontSize: fontSize!.fSize, height: isWalletAmountInput! ? 1 / 4 : 21 / 14, fontWeight: FontWeight.w500, color: AppColors.textColor, letterSpacing: -0.2), decoration: InputDecoration( isDense: true, hintText: hintText, hintStyle: TextStyle(fontSize: 14.fSize, height: 21 / 16, fontWeight: FontWeight.w500, color: Color(0xff898A8D), letterSpacing: -0.2), prefixIconConstraints: BoxConstraints(minWidth: 45.h), - prefixIcon: prefix == null - ? null - : Text( - "+" + prefix!, - style: TextStyle( - fontSize: 14.fSize, - height: 21 / 14, - fontWeight: FontWeight.w500, - color: Color(0xff2E303A), - letterSpacing: -0.2, + suffixIconConstraints: BoxConstraints(maxHeight: 20.h), + prefixIcon: prefix == null + ? null + : Text( + "+" + prefix!, + style: TextStyle( + fontSize: 14.fSize, + height: 21 / 14, + fontWeight: FontWeight.w500, + color: Color(0xff2E303A), + letterSpacing: -0.2, + ), ), - ), + suffixIcon: suffix != null + ? Padding( + padding: EdgeInsets.only(right: 0.h), + child: suffix, + ) + : null, contentPadding: EdgeInsets.zero, border: InputBorder.none, focusedBorder: InputBorder.none, diff --git a/lib/widgets/loader/bottomsheet_loader.dart b/lib/widgets/loader/bottomsheet_loader.dart index 57dfa01..aa97fdf 100644 --- a/lib/widgets/loader/bottomsheet_loader.dart +++ b/lib/widgets/loader/bottomsheet_loader.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; @@ -17,7 +18,7 @@ class LoaderBottomSheet { showModalBottomSheet( context: _navService.navigatorKey.currentContext!, - isDismissible: ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? true : false, + isDismissible: (ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat || kDebugMode) ? true : false, enableDrag: false, backgroundColor: Colors.transparent, builder: (_) { diff --git a/pubspec.yaml b/pubspec.yaml index a635cd1..055fb7b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,7 +65,7 @@ dependencies: google_api_availability: ^5.0.1 firebase_analytics: ^11.5.1 jiffy: ^6.4.3 - hijri_gregorian_calendar: ^0.1.0 + hijri_gregorian_calendar: ^0.1.1 web: any flutter_staggered_animations: ^1.1.1 @@ -79,6 +79,7 @@ dependencies: path_provider: ^2.0.8 open_filex: ^4.7.0 flutter_swiper_view: ^1.1.8 + family_bottom_sheet: ^0.1.0 dev_dependencies: flutter_test: