aboutsummaryrefslogtreecommitdiff
path: root/front/src
diff options
context:
space:
mode:
authorGuillermo Ramos2025-03-16 13:30:13 +0100
committerGuillermo Ramos2025-03-16 20:00:39 +0100
commit18e4ca440cf1b9d8d20e3e24bac0c55bbd9efada (patch)
treeef4b16f7ab0d91bc0e9256c75907d3877be4d74c /front/src
parent069e902819955d26adc8045a760c1ccfa4be549e (diff)
downloadhiccup-18e4ca440cf1b9d8d20e3e24bac0c55bbd9efada.tar.gz
Add updates
Diffstat (limited to 'front/src')
-rw-r--r--front/src/Main.elm349
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
]
]
]