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 ] ] ] |