module Main exposing (..) import Round import Browser import Html exposing (Html, button, div, input, text) import Html.Attributes exposing (max, min, step, type_, value) 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) 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 SimSpecs = { principal : Float , i1 : Float , years : Int } type alias Model = { error : String , simSpecs : SimSpecs , 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 simSpecs = { principal = 200000.0, i1 = 1.621, years = 30 } req = runSimulation simSpecs in ( { error = "", simSpecs = simSpecs, simulation = Nothing }, req ) -- UPDATE type SimSpecUpdate = Principal | I1 | Years type Msg = GotSimulation (Result Http.Error Simulation) | UpdateSimSpecs SimSpecUpdate String | RunSimulation 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 -> ( model, runSimulation model.simSpecs ) UpdateSimSpecs u val -> let simSpecs = model.simSpecs m = case u of Principal -> case String.toFloat val of Just p -> { model | simSpecs = { simSpecs | principal = p } } Nothing -> { model | error = "Error parsing principal" } I1 -> case String.toFloat val of Just i -> { model | simSpecs = { simSpecs | i1 = i } } Nothing -> { model | error = "Error parsing interest" } Years -> case String.toInt val of Just i -> { model | simSpecs = { simSpecs | years = i } } Nothing -> { model | error = "Error parsing years" } in ( m, 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 : SimSpecs -> Html Msg specsView { principal, i1, years } = div [] [ div [] [ text "Principal: " , input [ type_ "range" , Html.Attributes.min "0" , Html.Attributes.max "1000000" , step "10000" , value (String.fromFloat principal) , onInput (UpdateSimSpecs Principal) ] [] , text (Round.round 2 principal) ] , div [] [ text "Interest rate: " , input [ Html.Attributes.min "0" , Html.Attributes.max "100" , value (Round.round 2 i1) , onInput (UpdateSimSpecs I1) ] [] ] , div [] [ text "Years: " , input [ type_ "range" , Html.Attributes.min "1" , Html.Attributes.max "50" , step "1" , value (String.fromInt years) , onInput (UpdateSimSpecs Years) ] [] , text (String.fromInt years) ] , button [ onClick RunSimulation ] [ text "Simulate" ] ] quotaView : Quota -> Html Msg quotaView { period, payed, pending_principal } = div [] [ text (String.join "\t" [ String.fromInt period, capitalStr payed, Round.round 2 pending_principal ]) ] historyView : List Quota -> Html Msg historyView quotas = div [] (List.map quotaView quotas) simView : Simulation -> Html Msg simView { history, topay, payed } = div [] [ historyView history , div [] [ text (String.concat [ "to pay: ", capitalStr topay ]) ] , div [] [ text (String.concat [ "payed: ", capitalStr payed ]) ] ] view : Model -> Html Msg view model = div [] [ specsView model.simSpecs , case model.simulation of Nothing -> text "" Just sim -> div [] [ simView sim ] , div [] [ text model.error ] ]