module Main exposing (main) import Round import Browser import Html exposing (Html, button, div, input, text, hr) import Html.Attributes exposing (max, min, step, type_, value, disabled) import Html.Events exposing (onClick, onInput) import Http import Json.Decode exposing (Decoder, field, float, int, list, map, map2, map3, map4) import Json.Encode as Encode exposing (Value, object) -- MAIN main = Browser.element { init = init, update = update, view = view, subscriptions = \_ -> Sub.none } -- MODEL type alias Capital = { principal : Float , interest : Float } capitalDecoder : Decoder Capital capitalDecoder = map2 Capital (field "principal" float) (field "interest" float) capitalSumStr : Capital -> String capitalSumStr { principal, interest } = Round.round 2 (principal + interest) capitalStr : Capital -> String capitalStr { principal, interest } = String.concat [ "{principal=", Round.round 2 principal, ", interest=", Round.round 2 interest, "}" ] type alias Quota = { period : Int , payed : Capital , pending_principal : Float } quotaDecoder : Decoder Quota quotaDecoder = map3 Quota (field "period" int) (field "payed" capitalDecoder) (field "pending_principal" float) type alias Simulation = { history : List Quota , topay : Capital , payed : Capital , payed_amortized : Float } simDecoder : Decoder Simulation simDecoder = map4 Simulation (field "history" (list quotaDecoder)) (field "topay" capitalDecoder) (field "payed" capitalDecoder) (field "payed_amortized" float) type alias SimSpecsTxt = { principalTxt : String , i1Txt : String , yearsTxt : String } simSpecsParse : SimSpecsTxt -> Maybe SimSpecs simSpecsParse { principalTxt, i1Txt, yearsTxt } = case (String.toFloat principalTxt, String.toFloat i1Txt, String.toInt yearsTxt) of (Just principal, Just i1, Just years) -> Just { principal = principal, i1 = i1, years = years } _ -> Nothing type alias SimSpecs = { principal : Float , i1 : Float , years : Int } type alias Model = { error : String , simSpecsTxt : SimSpecsTxt , simulation : Maybe Simulation } qs : List ( String, String ) -> String qs ss = String.join "&" (List.map (\( s, t ) -> String.join "=" [ s, t ]) ss) simSpecsToURL : SimSpecs -> String simSpecsToURL { principal, i1, years } = let base = "/api/simulate" in String.concat [ base , "?" , qs [ ( "principal", String.fromFloat principal ) , ( "i1" , String.fromFloat (i1 / 100) ) , ( "years", String.fromInt years ) ] ] runSimulation : SimSpecs -> Cmd Msg runSimulation simSpecs = Http.get { url = simSpecsToURL simSpecs , expect = Http.expectJson GotSimulation simDecoder } init : () -> ( Model, Cmd Msg ) init () = let simSpecsTxt = { principalTxt = "200000", i1Txt = "1.621", yearsTxt = "30" } in ( { error = "", simSpecsTxt = simSpecsTxt, simulation = Nothing }, Cmd.none ) -- UPDATE type SimSpecUpdate = Principal | I1 | Years type Msg = GotSimulation (Result Http.Error Simulation) | UpdateSimSpecs SimSpecUpdate String | RunSimulation SimSpecs update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = let _ = Debug.log "UPDATE!" msg in case msg of GotSimulation (Ok simulation) -> ( { model | simulation = Just simulation }, Cmd.none ) GotSimulation (Err err) -> ( { model | error = errorToString err }, Cmd.none ) RunSimulation specs -> (model, runSimulation specs) UpdateSimSpecs u val -> let simSpecsTxt = model.simSpecsTxt newSimSpecsTxt = case u of Principal -> { simSpecsTxt | principalTxt = val } I1 -> { simSpecsTxt | i1Txt = val } Years -> { simSpecsTxt | yearsTxt = val } in ( { model | simSpecsTxt = newSimSpecsTxt }, Cmd.none ) -- VIEW errorToString : Http.Error -> String errorToString error = case error of Http.BadUrl url -> "The URL " ++ url ++ " was invalid" Http.Timeout -> "Unable to reach the server, try again" Http.NetworkError -> "Unable to reach the server, check your network connection" Http.BadStatus 500 -> "The server had a problem, try again later" Http.BadStatus 400 -> "Verify your information and try again" Http.BadStatus _ -> "Unknown error" Http.BadBody errorMessage -> errorMessage specsView : SimSpecsTxt -> Html Msg specsView simSpecsTxt = let { principalTxt, i1Txt, yearsTxt } = simSpecsTxt simSpecs = simSpecsParse simSpecsTxt simButtonAttrs = case simSpecs of Nothing -> [ disabled True ] Just specs -> [ onClick (RunSimulation specs) ] in div [] [ div [] [ text "Principal: " , input [ type_ "range" , Html.Attributes.min "0" , Html.Attributes.max "1000000" , step "10000" , value principalTxt , onInput (UpdateSimSpecs Principal) ] [] , text principalTxt ] , div [] [ text "Interest rate: " , input [ Html.Attributes.min "0" , Html.Attributes.max "100" , value i1Txt , onInput (UpdateSimSpecs I1) ] [] ] , div [] [ text "Years: " , input [ type_ "range" , Html.Attributes.min "1" , Html.Attributes.max "50" , step "1" , value yearsTxt , onInput (UpdateSimSpecs Years) ] [] , text yearsTxt ] , button simButtonAttrs [ text "Simulate" ] ] quotaView : Quota -> Html Msg quotaView { period, payed, pending_principal } = if modBy 12 period == 0 then div [] [ text (String.join "\t" [ String.fromInt period, capitalSumStr payed, capitalStr payed, Round.round 2 pending_principal ]) ] else text "" historyView : List Quota -> Html Msg historyView quotas = div [] (List.map quotaView quotas) simView : Simulation -> Html Msg simView { history, topay, payed } = div [] [ hr [] [] , historyView history , hr [] [] , div [] [ text (String.concat [ "to pay: ", capitalSumStr topay, " (", capitalStr topay, ")" ]) ] , div [] [ text (String.concat [ "payed: ", capitalSumStr payed, " (", capitalStr payed, ")" ]) ] ] view : Model -> Html Msg view model = div [] [ specsView model.simSpecsTxt , case model.simulation of Nothing -> text "" Just sim -> div [] [ simView sim ] , div [] [ text model.error ] ]