diff options
| author | Guillermo Ramos | 2025-03-16 13:30:13 +0100 | 
|---|---|---|
| committer | Guillermo Ramos | 2025-03-16 20:00:39 +0100 | 
| commit | 18e4ca440cf1b9d8d20e3e24bac0c55bbd9efada (patch) | |
| tree | ef4b16f7ab0d91bc0e9256c75907d3877be4d74c /front/src | |
| parent | 069e902819955d26adc8045a760c1ccfa4be549e (diff) | |
| download | hiccup-18e4ca440cf1b9d8d20e3e24bac0c55bbd9efada.tar.gz | |
Add updates
Diffstat (limited to 'front/src')
| -rw-r--r-- | front/src/Main.elm | 349 | 
1 files changed, 304 insertions, 45 deletions
diff --git a/front/src/Main.elm b/front/src/Main.elm index df5e719..ad43e46 100644 --- a/front/src/Main.elm +++ b/front/src/Main.elm @@ -105,6 +105,9 @@ make_t lang str =                  "Payed early: " ->                      "Anticipado: " +                "Payed: " -> +                    "Amort: " +                  "Saved: " ->                      "Ahorro: " @@ -117,6 +120,27 @@ make_t lang str =                  "Financed (mortgage): " ->                      "Financiado (hipoteca): " +                "Remove" -> +                    "Eliminar" + +                "Early payment" -> +                    "Amortización anticipada" + +                "New interest rate" -> +                    "Nuevo tipo de interés" + +                "Prepay:" -> +                    "Amortizar:" + +                "Interest:" -> +                    "Interés:" + +                "Cancel" -> +                    "Cancelar" + +                "Apply" -> +                    "Aplicar" +                  "Year" ->                      "Año" @@ -129,9 +153,18 @@ make_t lang str =                  "Pending" ->                      "Pendiente" +                "(reset)" -> +                    "(resetear)" + +                "Periodic update" -> +                    "Actualización periódica" +                  "Updates" ->                      "Actualizaciones" +                "Invalid input" -> +                    "Datos inválidos" +                  _ ->                      str @@ -484,7 +517,7 @@ updatesInMonth { periodic, byMonth } month =              List.filter (periodicUpdateInMonth month) periodic          newByMonth = -            List.filter (\( m, updates ) -> m == month) byMonth +            List.filter (\( m, _ ) -> m == month) byMonth      in      { periodic = newPeriodically, byMonth = newByMonth } @@ -494,24 +527,42 @@ simUpdatesEncode { periodic, byMonth } =      JE.object          [ ( "periodic", JE.list periodicUpdateEncode periodic )          , ( "by_month" -          , JE.object <| -                List.map (\( m, us ) -> ( String.fromInt m, simUpdateEncode us )) byMonth +          , JE.list (\( m, us ) -> JE.list identity [ JE.int m, simUpdateEncode us ]) byMonth            )          ]  simUpdatesDecoder : JD.Decoder SimUpdates  simUpdatesDecoder = +    let +        monthUpdateDecoder = +            JD.map2 Tuple.pair (JD.index 0 JD.int) (JD.index 1 simUpdateDecoder) +    in      JD.map2 SimUpdates          (JD.field "periodic" (JD.list periodicUpdateDecoder)) -        (JD.field "by_month" -            (JD.keyValuePairs simUpdateDecoder -                |> JD.map -                    (\l -> -                        List.map (\( k, v ) -> ( Maybe.withDefault 0 (String.toInt k), v )) l -                    ) -            ) -        ) +        (JD.field "by_month" (JD.list monthUpdateDecoder)) + + + +-- (JD.list JD.string +--     |> JD.map +--         (\( m, u ) -> +--             ( Maybe.withDefault 0 +--                 (String.toInt +--                     m +--                 ) +--             , simUpdateDecoder u +--             ) +--         ) +-- ) +-- ) +-- ) +-- (simUpdateDecoder +--     >> JD.map +--         (List.map (\( k, v ) -> ( Maybe.withDefault 0 (String.toInt k), v ))) +-- ) +-- ) +-- )  type alias SimSpecs = @@ -672,12 +723,17 @@ settingsToQS { lang, currency } =      ] +type Editing +    = AddUpdate { type_ : SimUpdate, txt : String, month : Int, error : String } + +  type alias Model =      { settings : Settings      , navKey : Nav.Key      , error : String      , rawSpecs : RawSpecs      , expandedYears : Set Int +    , editing : Maybe Editing      , simulation : Maybe ( RawSpecs, MortgageSim )      , t : String -> String      } @@ -726,6 +782,7 @@ init () url navKey =      , error = ""      , rawSpecs = defaultRawSpecs      , expandedYears = Set.empty +    , editing = Nothing      , simulation = Nothing      , t = identity      } @@ -807,6 +864,23 @@ type Msg      | SetExpandedYears (Set Int)      | RmPeriodicUpdate PeriodicUpdate      | RmUpdate ( Int, SimUpdate ) +    | ResetUpdates +    | SetEditing (Maybe Editing) +    | CommitEditing + + +delete : a -> List a -> List a +delete x l = +    case l of +        [] -> +            [] + +        h :: t -> +            if x == h then +                t + +            else +                h :: delete x t  update : Msg -> Model -> ( Model, Cmd Msg ) @@ -866,7 +940,7 @@ update msg m =                      updates.periodic                  newM = -                    setModelUpdates { updates | periodic = List.filter ((/=) pu) periodicUpdates } m +                    setModelUpdates { updates | periodic = delete pu periodicUpdates } m              in              ( m, Nav.pushUrl m.navKey (modelToUrl newM) ) @@ -879,7 +953,7 @@ update msg m =                      updates.byMonth                  newM = -                    setModelUpdates { updates | byMonth = List.filter ((/=) mu) byMonUpdates } m +                    setModelUpdates { updates | byMonth = delete mu byMonUpdates } m              in              ( m, Nav.pushUrl m.navKey (modelToUrl newM) ) @@ -931,9 +1005,58 @@ update msg m =              in              ( { m | rawSpecs = newRawSpecs }, Cmd.none ) +        ResetUpdates -> +            let +                rawSpecs = +                    m.rawSpecs + +                newM = +                    { m | rawSpecs = { rawSpecs | updates = defaultSimUpdates } } +            in +            ( m, Nav.pushUrl m.navKey (modelToUrl newM) ) +          SetExpandedYears eyears ->              ( { m | expandedYears = eyears }, Cmd.none ) +        SetEditing editing -> +            ( { m | editing = editing }, Cmd.none ) + +        CommitEditing -> +            case m.editing of +                Just (AddUpdate au) -> +                    case String.toFloat au.txt of +                        Just f -> +                            let +                                u = +                                    case au.type_ of +                                        Amortize _ -> +                                            Amortize f + +                                        SetI1 _ -> +                                            SetI1 (f / 100) + +                                updates = +                                    m.rawSpecs.updates + +                                byMonth = +                                    ( au.month, u ) :: updates.byMonth + +                                newM = +                                    { m | editing = Nothing } +                                        |> setModelUpdates { updates | byMonth = byMonth } +                            in +                            ( newM +                            , Nav.pushUrl m.navKey (modelToUrl newM) +                            ) + +                        Nothing -> +                            ( { m | editing = Just (AddUpdate { au | error = m.t "Invalid input" }) } +                            , Cmd.none +                            ) + +                _ -> +                    ( m, Cmd.none ) +          UpdateSettings change ->              let                  settings = @@ -970,14 +1093,24 @@ update msg m =  -- VIEW (THEME) +errorTxt : String -> Html Msg +errorTxt error = +    span [ class "text-sm text-rose-600" ] [ text error ] + +  primaryButAttrs : List (Attribute Msg)  primaryButAttrs = -    [ class "px-3 rounded-md bg-lime-300 enabled:active:bg-lime-400 border border-lime-600 disabled:opacity-75" ] +    [ class "px-1 rounded-md bg-lime-300 enabled:active:bg-lime-400 border border-lime-600 disabled:opacity-75", style "cursor" "pointer" ]  secondaryButAttrs : List (Attribute Msg)  secondaryButAttrs = -    [ class "px-3 rounded-md text-gray-700 enabled:active:bg-lime-400 border border-2 border-gray-500 disabled:opacity-75" ] +    [ class "px-1 rounded-md text-gray-600 enabled:active:bg-gray-300 border border-gray-400 disabled:opacity-75", style "cursor" "pointer" ] + + +tertiaryButAttrs : List (Attribute Msg) +tertiaryButAttrs = +    [ class "px-1 text-gray-600 enabled:active:bg-gray-300 disabled:opacity-75", style "cursor" "pointer" ]  clickableAttrs : Msg -> List (Attribute Msg) @@ -985,6 +1118,16 @@ clickableAttrs msg =      [ onClick msg, class "text-lime-600", style "cursor" "pointer" ] +prepayAttrs : List (Attribute Msg) +prepayAttrs = +    [ class "bg-emerald-100 border-emerald-800" ] + + +i1Attrs : List (Attribute Msg) +i1Attrs = +    [ class "bg-yellow-100 border-yellow-800" ] + +  txtInput : List (Attribute Msg) -> (String -> Msg) -> String -> Html Msg  txtInput attributes onInputMsg valueTxt =      input @@ -1106,7 +1249,7 @@ titledAttrs title_ =  specsView : Model -> Html Msg  specsView { t, settings, rawSpecs } =      let -        { title, total, rate, initial, i1, years, vat, fee } = +        { title, total, rate, initial, i1, years, vat, fee, updates } =              rawSpecs          simButAttrs = @@ -1184,37 +1327,51 @@ specsView { t, settings, rawSpecs } =                  ]              ]          , div [ class "flex justify-between my-1 mt-2" ] -            [ button (primaryButAttrs ++ simButAttrs) [ text (t "Simulate") ] +            [ button (primaryButAttrs ++ simButAttrs ++ [ class "px-3" ]) [ text (t "Simulate") ]              , div [ class "flex" ] -                [ button (secondaryButAttrs ++ [ class "mr-1", onClick (UpdateSettings ToggleLang) ]) +                [ button (secondaryButAttrs ++ [ class "px-3 mr-1", onClick (UpdateSettings ToggleLang) ])                      [ text <| langToString settings.lang ] -                , button (secondaryButAttrs ++ [ class "mr-1", onClick (UpdateSettings ToggleCurrency) ]) +                , button (secondaryButAttrs ++ [ class "px-3 mr-1", onClick (UpdateSettings ToggleCurrency) ])                      [ text <| currencySymbol settings.currency ]                  ]              ]          ] -monthToYear : Int -> Int -monthToYear month = -    ((month - 1) // 12) + 1 - - -simUpdateView : List (Attribute Msg) -> Model -> SimUpdate -> Msg -> Html Msg -simUpdateView attrs m upd onClick = +simUpdateView : Model -> Bool -> SimUpdate -> Msg -> Html Msg +simUpdateView m periodic upd onClick =      let -        els = +        ( title_, attrs, els ) =              case upd of                  Amortize f -> -                    [ text "+", amountView [] m.settings.currency f ] +                    ( m.t "Early payment" +                    , prepayAttrs +                    , [ text (m.t "Payed: ") +                      , amountView [] m.settings.currency f +                      ] +                    )                  SetI1 f -> -                    [ text <| String.fromFloat (f * 100), text "%" ] +                    ( m.t "New interest rate" +                    , i1Attrs +                    , [ text (m.t "Interest: ") +                      , text <| String.fromFloat (f * 100) +                      , text "%" +                      ] +                    ) + +        periodicIcon = +            if periodic then +                span [ title <| m.t "Periodic update" ] [ text "⟳ " ] + +            else +                text ""      in -    p (attrs ++ [ class "bg-lime-200 m-1" ]) -        (els +    span (attrs ++ [ title title_, class "px-1 rounded-md border border-1" ]) +        (periodicIcon +            :: els              ++ [ text " " -               , span (clickableAttrs onClick ++ [ class "text-red-600" ]) +               , span (clickableAttrs onClick ++ [ title (m.t "Remove"), class "text-red-600" ])                      [ text "×" ]                 ]          ) @@ -1227,7 +1384,7 @@ quotaView m { updates } { month, payed, pending_principal } =              updatesInMonth updates month          year = -            monthToYear month +            ((month - 1) // 12) + 1          monthInExpandedYear =              Set.member year m.expandedYears @@ -1240,22 +1397,115 @@ quotaView m { updates } { month, payed, pending_principal } =                  ( "+ ", Set.insert year m.expandedYears )          periodicUpdates = -            List.map (\pu -> simUpdateView [] m pu.upd (RmPeriodicUpdate pu)) monthUpdates.periodic +            List.map (\pu -> simUpdateView m True pu.upd (RmPeriodicUpdate pu)) monthUpdates.periodic          byMonUpdates = -            List.map (\mu -> simUpdateView [] m (Tuple.second mu) (RmUpdate mu)) monthUpdates.byMonth +            List.map (\mu -> simUpdateView m False (Tuple.second mu) (RmUpdate mu)) monthUpdates.byMonth -        ( yearField, updatesField ) = +        yearField =              if modBy 12 (month - 1) == 0 then -                ( div [] +                div []                      [ span (clickableAttrs (SetExpandedYears newExpandedYears)) [ text toggleYearIcon ]                      , text (String.fromInt year)                      ] -                , text "..." + +            else +                text "" + +        newUpdateButton = +            button +                (secondaryButAttrs +                    ++ [ onClick +                            << SetEditing +                            << Just +                            << AddUpdate +                         <| +                            { type_ = Amortize 0 +                            , txt = "" +                            , month = month +                            , error = "" +                            } +                       , title (m.t "Add update") +                       ]                  ) +                [ text "+" ] + +        setUpdate = +            SetEditing << Just << AddUpdate + +        newUpdateInput au = +            let +                ( commonAttrs, butAttrs, afterInput ) = +                    case au.type_ of +                        Amortize _ -> +                            ( prepayAttrs +                            , { prepay = [ class "bg-emerald-200" ] +                              , i1 = [ style "cursor" "pointer", onClick (setUpdate <| { au | type_ = SetI1 0 }) ] +                              } +                            , text <| currencySymbol m.settings.currency +                            ) + +                        SetI1 _ -> +                            ( i1Attrs +                            , { prepay = [ style "cursor" "pointer", onClick (setUpdate <| { au | type_ = Amortize 0 }) ] +                              , i1 = [ class "bg-yellow-200" ] +                              } +                            , text "%" +                            ) +            in +            div (commonAttrs ++ [ class "flex flex-col border border-1 rounded-md p-2" ]) +                [ div [ class "flex justify-center" ] +                    [ button +                        (butAttrs.prepay +                            ++ [ class "px-1 border border-1" +                               , title (m.t "Early payment") +                               ] +                        ) +                        [ text <| m.t "Prepay:" ] +                    , button +                        (butAttrs.i1 +                            ++ [ class "px-1 border border-1" +                               , title (m.t "New interest rate") +                               ] +                        ) +                        [ text <| m.t "Interest:" ] +                    ] +                , div [ class "py-1" ] +                    [ txtInput +                        [ class "w-[100px] border" ] +                        (\newTxt -> setUpdate { au | txt = newTxt }) +                        au.txt +                    , afterInput +                    ] +                , errorTxt au.error +                , div [ class "flex justify-end gap-1" ] +                    [ button (tertiaryButAttrs ++ [ onClick (SetEditing Nothing) ]) [ text <| m.t "Cancel" ] +                    , button (primaryButAttrs ++ [ onClick CommitEditing ]) [ text <| m.t "Apply" ] +                    ] +                ] + +        newUpdateButtonOrInput = +            case m.editing of +                Just (AddUpdate au) -> +                    if au.month == month then +                        newUpdateInput au + +                    else +                        newUpdateButton + +                _ -> +                    newUpdateButton + +        updatesField = +            if modBy 12 (month - 1) == 0 && not monthInExpandedYear then +                text "..."              else -                ( text "", div [] (periodicUpdates ++ byMonUpdates) ) +                div [ class "flex flex-wrap items-start gap-1" ] +                    (periodicUpdates +                        ++ byMonUpdates +                        ++ [ newUpdateButtonOrInput ] +                    )      in      if modBy 12 (month - 1) == 0 || monthInExpandedYear then          tr [] @@ -1275,16 +1525,25 @@ quotaView m { updates } { month, payed, pending_principal } =  mortgageView : Model -> MortgageSim -> Html Msg  mortgageView m sim =      let +        clearUpdates = +            if m.rawSpecs.updates == defaultSimUpdates then +                text "" + +            else +                button +                    (tertiaryButAttrs ++ [ class "text-sm", onClick ResetUpdates ]) +                    [ text (m.t "(reset)") ] +          titles = -            [ "Year", "Month", "Quota", "Pending", "Updates" ] +            List.map (\t -> [ text <| m.t t ]) [ "Year", "Month", "Quota", "Pending" ] +                ++ [ [ text <| m.t "Updates", clearUpdates ] ]          head =              thead [ class "bg-lime-100" ]                  [ tr []                      (List.map                          (\txt -> -                            th [ class "px-3 py-1 border border-gray-300" ] -                                [ text <| m.t txt ] +                            th [ class "px-3 py-1 border border-gray-300" ] txt                          )                          titles                      ) @@ -1324,7 +1583,7 @@ simView m ( rawSpecs, sim ) =          overview title financed extraLis =              div [ class "my-2 p-1 border-2 rounded-md border-gray-400" ] -                [ p [ class "text-lg" ] +                [ p []                      [ text title                      , amountView [ class "font-bold" ]                          currency @@ -1392,7 +1651,7 @@ view : Model -> Document Msg  view m =      { title = "Hiccup"      , body = -        [ div [ class "flex flex-col max-w-xl mx-auto items-center mt-2 p-3 border-2 rounded-md border-gray-500 bg-gray-100" ] +        [ div [ class "flex flex-col max-w-2xl mx-auto items-center mt-2 p-3 border-2 rounded-md border-gray-500 bg-gray-100" ]              [ div [ class "min-w-full" ]                  [ specsView m                  , case m.simulation of @@ -1401,7 +1660,7 @@ view m =                      Just sim ->                          simView m sim -                , span [ class "text-rose-600" ] [ text m.error ] +                , errorTxt m.error                  ]              ]          ]  | 
