From cfca18262c1ff48dcb683ddab7d03cf8e55573ff Mon Sep 17 00:00:00 2001 From: Joris Date: Fri, 24 Mar 2017 09:21:04 +0000 Subject: Features/categories --- src/client/elm/Dialog.elm | 3 +- src/client/elm/Dialog/AddCategory/Model.elm | 53 +++++++++ src/client/elm/Dialog/AddCategory/View.elm | 72 +++++++++++++ src/client/elm/Dialog/AddIncome/Model.elm | 27 ++--- src/client/elm/Dialog/AddIncome/View.elm | 9 +- src/client/elm/Dialog/AddPayment/Model.elm | 57 +++++----- src/client/elm/Dialog/AddPayment/View.elm | 41 ++++--- src/client/elm/Dialog/Model.elm | 3 + src/client/elm/Dialog/Msg.elm | 7 +- src/client/elm/Dialog/Update.elm | 51 ++++++++- src/client/elm/Init.elm | 16 +-- src/client/elm/LoggedData.elm | 6 ++ src/client/elm/LoggedIn/Category/Model.elm | 36 +++++++ src/client/elm/LoggedIn/Category/Msg.elm | 9 ++ src/client/elm/LoggedIn/Category/Table/View.elm | 124 +++++++++++++++++++++ src/client/elm/LoggedIn/Category/Update.elm | 24 +++++ src/client/elm/LoggedIn/Category/View.elm | 35 ++++++ src/client/elm/LoggedIn/Home/Header/View.elm | 7 +- src/client/elm/LoggedIn/Home/Model.elm | 15 +-- src/client/elm/LoggedIn/Home/Update.elm | 4 +- src/client/elm/LoggedIn/Home/View.elm | 21 ++-- src/client/elm/LoggedIn/Home/View/Paging.elm | 74 +++++++------ src/client/elm/LoggedIn/Home/View/Table.elm | 42 ++++++-- src/client/elm/LoggedIn/Income/Model.elm | 15 +-- src/client/elm/LoggedIn/Income/Update.elm | 12 +-- src/client/elm/LoggedIn/Income/View.elm | 33 +++--- src/client/elm/LoggedIn/Income/View/Table.elm | 6 +- src/client/elm/LoggedIn/Model.elm | 21 ++-- src/client/elm/LoggedIn/Msg.elm | 18 ++-- src/client/elm/LoggedIn/Stat/View.elm | 4 +- src/client/elm/LoggedIn/Update.elm | 80 +++++++++----- src/client/elm/LoggedIn/View.elm | 16 +-- src/client/elm/Main.elm | 7 +- src/client/elm/Model.elm | 22 ++-- src/client/elm/Model/Category.elm | 35 ++++++ src/client/elm/Model/Conf.elm | 4 +- src/client/elm/Model/Date.elm | 8 +- src/client/elm/Model/Income.elm | 37 +++---- src/client/elm/Model/Init.elm | 22 ++-- src/client/elm/Model/InitResult.elm | 16 +-- src/client/elm/Model/Payer.elm | 9 +- src/client/elm/Model/Payment.elm | 61 ++++++----- src/client/elm/Model/PaymentCategory.elm | 48 +++++++++ src/client/elm/Model/Size.elm | 10 +- src/client/elm/Model/Translations.elm | 25 +++-- src/client/elm/Model/User.elm | 24 ++--- src/client/elm/Msg.elm | 20 ++-- src/client/elm/Page.elm | 29 +++-- src/client/elm/Server.elm | 138 ++++++++++++++---------- src/client/elm/SignIn/View.elm | 4 +- src/client/elm/Update.elm | 134 +++++++++++++---------- src/client/elm/Utils/Cmd.elm | 4 +- src/client/elm/Utils/Http.elm | 80 +++++--------- src/client/elm/Utils/Json.elm | 12 +++ src/client/elm/Utils/Maybe.elm | 19 ++-- src/client/elm/Utils/Search.elm | 10 ++ src/client/elm/Utils/String.elm | 38 +++++++ src/client/elm/Utils/Tuple.elm | 14 --- src/client/elm/Validation.elm | 20 +++- src/client/elm/View.elm | 1 - src/client/elm/View/Events.elm | 4 +- src/client/elm/View/Form.elm | 89 +++++++++++---- src/client/elm/View/Header.elm | 1 + 63 files changed, 1306 insertions(+), 580 deletions(-) create mode 100644 src/client/elm/Dialog/AddCategory/Model.elm create mode 100644 src/client/elm/Dialog/AddCategory/View.elm create mode 100644 src/client/elm/LoggedIn/Category/Model.elm create mode 100644 src/client/elm/LoggedIn/Category/Msg.elm create mode 100644 src/client/elm/LoggedIn/Category/Table/View.elm create mode 100644 src/client/elm/LoggedIn/Category/Update.elm create mode 100644 src/client/elm/LoggedIn/Category/View.elm create mode 100644 src/client/elm/Model/Category.elm create mode 100644 src/client/elm/Model/PaymentCategory.elm create mode 100644 src/client/elm/Utils/Json.elm create mode 100644 src/client/elm/Utils/Search.elm create mode 100644 src/client/elm/Utils/String.elm delete mode 100644 src/client/elm/Utils/Tuple.elm (limited to 'src/client/elm') diff --git a/src/client/elm/Dialog.elm b/src/client/elm/Dialog.elm index 3b9e93b..a7e059a 100644 --- a/src/client/elm/Dialog.elm +++ b/src/client/elm/Dialog.elm @@ -65,8 +65,7 @@ update updateModel msg baseModel model = UpdateAndClose msg -> ( { model | config = Nothing } - , Task.succeed () - |> Task.perform (always msg) (always msg) + , Task.perform (always msg) (Task.succeed msg) ) OpenWithUpdate config modelMsg -> diff --git a/src/client/elm/Dialog/AddCategory/Model.elm b/src/client/elm/Dialog/AddCategory/Model.elm new file mode 100644 index 0000000..8aeec1a --- /dev/null +++ b/src/client/elm/Dialog/AddCategory/Model.elm @@ -0,0 +1,53 @@ +module Dialog.AddCategory.Model exposing + ( Model + , init + , initialAdd + , initialClone + , initialEdit + , validation + ) + +import Date exposing (Date) +import View.Date as Date + +import Form exposing (Form) +import Form.Field as Field exposing (Field) +import Form.Validate as Validate exposing (Validation) +import Validation + +import Model.Translations exposing (Translations) +import Model.Category exposing (Category, CategoryId) + +type alias Model = + { id : Maybe CategoryId + , name : String + , color : String + } + +init : Form String Model +init = Form.initial [] validation + +initialAdd : Translations -> List (String, Field) +initialAdd translations = + [ ("color", Field.string "#000000") + ] + +initialClone : Translations -> Category -> List (String, Field) +initialClone translations category = + [ ("name", Field.string category.name) + , ("color", Field.string category.color) + ] + +initialEdit : Translations -> CategoryId -> Category -> List (String, Field) +initialEdit translations categoryId category = + [ ("id", Field.string (toString categoryId)) + , ("name", Field.string category.name) + , ("color", Field.string category.color) + ] + +validation : Validation String Model +validation = + Validate.map3 Model + (Validate.field "id" (Validate.maybe Validate.int)) + (Validate.field "name" (Validate.string |> Validate.andThen Validate.nonEmpty)) + (Validate.field "color" (Validate.string |> Validate.andThen Validate.nonEmpty)) diff --git a/src/client/elm/Dialog/AddCategory/View.elm b/src/client/elm/Dialog/AddCategory/View.elm new file mode 100644 index 0000000..6c02351 --- /dev/null +++ b/src/client/elm/Dialog/AddCategory/View.elm @@ -0,0 +1,72 @@ +module Dialog.AddCategory.View exposing + ( button + ) + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Task + +import Form exposing (Form) +import Form.Field as Field exposing (Field) +import Utils.Form as Form + +import Dialog +import Dialog.AddCategory.Model as AddCategory +import Dialog.Msg as DialogMsg + +import Tooltip + +import View.Form as Form +import View.Events exposing (onSubmitPrevDefault) + +import Msg exposing (Msg) +import LoggedIn.Msg as LoggedInMsg +import LoggedIn.Home.Msg as HomeMsg + +import Model.Translations exposing (getMessage) +import Model.View exposing (View(LoggedInView)) + +import LoggedData exposing (LoggedData) +import LoggedIn.Home.Model as HomeModel + +button : LoggedData -> List (String, Field) -> String -> Html Msg -> Maybe String -> Html Msg +button loggedData initialForm title buttonContent tooltip = + let dialogConfig = + { className = "categoryDialog" + , title = getMessage loggedData.translations title + , body = \model -> addCategoryForm loggedData model.addCategory + , confirm = getMessage loggedData.translations "Confirm" + , confirmMsg = submitForm << .addCategory + , undo = getMessage loggedData.translations "Undo" + } + in Html.button + ( ( case tooltip of + Just message -> Tooltip.show Msg.Tooltip message + Nothing -> [] + ) + ++ [ onClick (Msg.Dialog <| Dialog.OpenWithUpdate dialogConfig (DialogMsg.Init "categoryname" (DialogMsg.AddCategoryMsg <| Form.Reset initialForm))) ] + ) + [ buttonContent ] + +addCategoryForm : LoggedData -> Form String AddCategory.Model -> Html Msg +addCategoryForm loggedData addCategory = + let htmlMap = Html.map (Msg.Dialog << Dialog.Update << DialogMsg.AddCategoryMsg) + in Html.form + [ onSubmitPrevDefault Msg.NoOp ] + [ htmlMap <| Form.textInput loggedData.translations addCategory "category" "name" + , htmlMap <| Form.colorInput loggedData.translations addCategory "category" "color" + , Form.hiddenSubmit (submitForm addCategory) + ] + +submitForm : Form String AddCategory.Model -> Msg +submitForm addCategory = + case Form.getOutput addCategory of + Just data -> + case data.id of + Just categoryId -> + Msg.Dialog <| Dialog.UpdateAndClose <| Msg.EditCategory categoryId data.name data.color + Nothing -> + Msg.Dialog <| Dialog.UpdateAndClose <| Msg.CreateCategory data.name data.color + Nothing -> + Msg.Dialog <| Dialog.Update <| DialogMsg.AddCategoryMsg <| Form.Submit diff --git a/src/client/elm/Dialog/AddIncome/Model.elm b/src/client/elm/Dialog/AddIncome/Model.elm index 0d52b22..ad7b25a 100644 --- a/src/client/elm/Dialog/AddIncome/Model.elm +++ b/src/client/elm/Dialog/AddIncome/Model.elm @@ -4,6 +4,7 @@ module Dialog.AddIncome.Model exposing , initialAdd , initialClone , initialEdit + , validation ) import Date exposing (Date) @@ -24,29 +25,29 @@ type alias Model = } init : Form String Model -init = Form.initial [] validate +init = Form.initial [] validation initialAdd : Translations -> Date -> List (String, Field) initialAdd translations date = - [ ("date", Field.Text (Date.shortView date translations)) + [ ("date", Field.string (Date.shortView date translations)) ] initialClone : Translations -> Date -> Income -> List (String, Field) initialClone translations date income = - [ ("amount", Field.Text (toString income.amount)) - , ("date", Field.Text (Date.shortView date translations)) + [ ("amount", Field.string (toString income.amount)) + , ("date", Field.string (Date.shortView date translations)) ] initialEdit : Translations -> IncomeId -> Income -> List (String, Field) initialEdit translations incomeId income = - [ ("id", Field.Text (toString incomeId)) - , ("amount", Field.Text (toString income.amount)) - , ("date", Field.Text (Date.shortView (Date.fromTime income.time) translations)) + [ ("id", Field.string (toString incomeId)) + , ("amount", Field.string (toString income.amount)) + , ("date", Field.string (Date.shortView (Date.fromTime income.time) translations)) ] -validate : Validation String Model -validate = - Validate.form3 Model - (Validate.get "id" (Validate.maybe Validate.int)) - (Validate.get "amount" (Validate.int `Validate.andThen` (Validate.minInt 1))) - (Validate.get "date" Validation.date) +validation : Validation String Model +validation = + Validate.map3 Model + (Validate.field "id" (Validate.maybe Validate.int)) + (Validate.field "amount" (Validate.int |> Validate.andThen (Validate.minInt 1))) + (Validate.field "date" Validation.date) diff --git a/src/client/elm/Dialog/AddIncome/View.elm b/src/client/elm/Dialog/AddIncome/View.elm index c628d37..b413308 100644 --- a/src/client/elm/Dialog/AddIncome/View.elm +++ b/src/client/elm/Dialog/AddIncome/View.elm @@ -5,7 +5,6 @@ module Dialog.AddIncome.View exposing import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (..) -import Html.App as Html import Task import Form exposing (Form) @@ -31,8 +30,8 @@ import Model.View exposing (View(LoggedInView)) import LoggedData exposing (LoggedData) import LoggedIn.Home.Model as HomeModel -button : String -> LoggedData -> List (String, Field) -> String -> Html Msg -> Maybe String -> Html Msg -button className loggedData initialForm title buttonContent tooltip = +button : LoggedData -> List (String, Field) -> String -> Html Msg -> Maybe String -> Html Msg +button loggedData initialForm title buttonContent tooltip = let dialogConfig = { className = "incomeDialog" , title = getMessage loggedData.translations title @@ -46,9 +45,7 @@ button className loggedData initialForm title buttonContent tooltip = Just message -> Tooltip.show Msg.Tooltip message Nothing -> [] ) - ++ [ class className - , onClick (Msg.Dialog <| Dialog.OpenWithUpdate dialogConfig (DialogMsg.AddIncomeMsg <| Form.Reset initialForm)) - ] + ++ [ onClick (Msg.Dialog <| Dialog.OpenWithUpdate dialogConfig (DialogMsg.Init "incomeamount" (DialogMsg.AddIncomeMsg <| Form.Reset initialForm))) ] ) [ buttonContent ] diff --git a/src/client/elm/Dialog/AddPayment/Model.elm b/src/client/elm/Dialog/AddPayment/Model.elm index 19326f0..a287d37 100644 --- a/src/client/elm/Dialog/AddPayment/Model.elm +++ b/src/client/elm/Dialog/AddPayment/Model.elm @@ -4,6 +4,7 @@ module Dialog.AddPayment.Model exposing , initialAdd , initialClone , initialEdit + , validation ) import Date exposing (Date) @@ -16,46 +17,54 @@ import Validation import Model.Payment as Payment exposing (Payment, Frequency, PaymentId) import Model.Translations exposing (Translations) +import Model.Category as Category exposing (Categories, CategoryId) + +import Utils.Maybe as Maybe type alias Model = { id : Maybe PaymentId , name : String , cost : Int , date : Date + , category : CategoryId , frequency : Frequency } init : Form String Model -init = Form.initial [] validation +init = Form.initial [] (validation Category.empty) initialAdd : Translations -> Date -> Frequency -> List (String, Field) initialAdd translations date frequency = - [ ("date", Field.Text (Date.shortView date translations)) - , ("frequency", Field.Radio (toString frequency)) + [ ("date", Field.string (Date.shortView date translations)) + , ("frequency", Field.string (toString frequency)) + , ("category", Field.string "") ] -initialClone : Translations -> Date -> Payment -> List (String, Field) -initialClone translations date payment = - [ ("name", Field.Text payment.name) - , ("cost", Field.Text (toString payment.cost)) - , ("date", Field.Text (Date.shortView date translations)) - , ("frequency", Field.Radio (toString payment.frequency)) +initialClone : Translations -> Date -> Maybe CategoryId -> Payment -> List (String, Field) +initialClone translations date category payment = + [ ("name", Field.string payment.name) + , ("cost", Field.string (toString payment.cost)) + , ("date", Field.string (Date.shortView date translations)) + , ("frequency", Field.string (toString payment.frequency)) + , ("category", Field.string (Maybe.map toString category |> Maybe.withDefault "")) ] -initialEdit : Translations -> Payment -> List (String, Field) -initialEdit translations payment = - [ ("id", Field.Text (toString payment.id)) - , ("name", Field.Text payment.name) - , ("cost", Field.Text (toString payment.cost)) - , ("date", Field.Text (Date.shortView payment.date translations)) - , ("frequency", Field.Radio (toString payment.frequency)) +initialEdit : Translations -> Maybe CategoryId -> Payment -> List (String, Field) +initialEdit translations category payment = + [ ("id", Field.string (toString payment.id)) + , ("name", Field.string payment.name) + , ("cost", Field.string (toString payment.cost)) + , ("date", Field.string (Date.shortView payment.date translations)) + , ("frequency", Field.string (toString payment.frequency)) + , ("category", Field.string (Maybe.map toString category |> Maybe.withDefault "")) ] -validation : Validation String Model -validation = - Validate.form5 Model - (Validate.get "id" (Validate.maybe Validate.int)) - (Validate.get "name" (Validate.string `Validate.andThen` (Validate.nonEmpty))) - (Validate.get "cost" (Validate.int `Validate.andThen` (Validate.minInt 1))) - (Validate.get "date" Validation.date) - (Validate.get "frequency" Payment.validateFrequency) +validation : Categories -> Validation String Model +validation categories = + Validate.map6 Model + (Validate.field "id" (Validate.maybe Validate.int)) + (Validate.field "name" (Validate.string |> Validate.andThen Validate.nonEmpty)) + (Validate.field "cost" (Validate.int |> Validate.andThen (Validate.minInt 1))) + (Validate.field "date" Validation.date) + (Validate.field "category" (Validation.category categories)) + (Validate.field "frequency" Payment.validateFrequency) diff --git a/src/client/elm/Dialog/AddPayment/View.elm b/src/client/elm/Dialog/AddPayment/View.elm index df1ad3b..078d5b7 100644 --- a/src/client/elm/Dialog/AddPayment/View.elm +++ b/src/client/elm/Dialog/AddPayment/View.elm @@ -2,10 +2,10 @@ module Dialog.AddPayment.View exposing ( button ) +import Dict import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (..) -import Html.App as Html import Task import Form exposing (Form) @@ -18,15 +18,17 @@ import Dialog.Msg as DialogMsg import Tooltip -import View.Form as Form import View.Events exposing (onSubmitPrevDefault) +import View.Form as Form -import Msg exposing (Msg) -import LoggedIn.Msg as LoggedInMsg import LoggedIn.Home.Msg as HomeMsg +import LoggedIn.Msg as LoggedInMsg +import Msg exposing (Msg) -import Model.Translations exposing (getMessage) +import Model.Category exposing (Categories) import Model.Payment as Payment exposing (Frequency(..)) +import Model.PaymentCategory exposing (PaymentCategories) +import Model.Translations exposing (getMessage) import Model.View exposing (View(LoggedInView)) import LoggedData exposing (LoggedData) @@ -39,7 +41,7 @@ button loggedData initialForm title buttonContent tooltip = , title = getMessage loggedData.translations title , body = \model -> addPaymentForm loggedData model.addPayment , confirm = getMessage loggedData.translations "Confirm" - , confirmMsg = submitForm << .addPayment + , confirmMsg = submitForm loggedData.categories loggedData.paymentCategories << .addPayment , undo = getMessage loggedData.translations "Undo" } in Html.button @@ -48,14 +50,19 @@ button loggedData initialForm title buttonContent tooltip = Nothing -> [] ) ++ [ class "addPayment" - , onClick (Msg.Dialog <| Dialog.OpenWithUpdate dialogConfig (DialogMsg.AddPaymentMsg <| Form.Reset initialForm)) + , onClick (Msg.Dialog <| Dialog.OpenWithUpdate dialogConfig (DialogMsg.Init "paymentname" (DialogMsg.AddPaymentMsg loggedData.categories loggedData.paymentCategories <| Form.Reset initialForm))) ] ) [ buttonContent ] addPaymentForm : LoggedData -> Form String AddPayment.Model -> Html Msg addPaymentForm loggedData addPayment = - let htmlMap = Html.map (Msg.Dialog << Dialog.Update << DialogMsg.AddPaymentMsg) + let htmlMap = Html.map (Msg.Dialog << Dialog.Update << DialogMsg.AddPaymentMsg loggedData.categories loggedData.paymentCategories) + categoryOptions = + loggedData.categories + |> Dict.toList + |> List.sortBy (.name << Tuple.second) + |> List.map (\(id, category) -> (toString id, category.name)) in Html.form [ class "addPayment" , onSubmitPrevDefault Msg.NoOp @@ -65,18 +72,24 @@ addPaymentForm loggedData addPayment = , if (Form.getFieldAsString "frequency" addPayment).value == Just (toString Punctual) then htmlMap <| Form.textInput loggedData.translations addPayment "payment" "date" else text "" + , htmlMap <| Form.selectInput loggedData.translations addPayment "payment" "category" categoryOptions + , htmlMap <| Form.radioInputs loggedData.translations addPayment "payment" "frequency" [ toString Punctual, toString Monthly ] - , Form.hiddenSubmit (submitForm addPayment) + , Form.hiddenSubmit (submitForm loggedData.categories loggedData.paymentCategories addPayment) ] -submitForm : Form String AddPayment.Model -> Msg -submitForm addPayment = +submitForm : Categories -> PaymentCategories -> Form String AddPayment.Model -> Msg +submitForm categories paymentCategories addPayment = case Form.getOutput addPayment of Just data -> case data.id of Just paymentId -> - Msg.Dialog <| Dialog.UpdateAndClose <| Msg.EditPayment paymentId data.name data.cost data.date data.frequency + Msg.Dialog + <| Dialog.UpdateAndClose + <| Msg.EditPayment paymentId data.name data.cost data.date data.category data.frequency Nothing -> - Msg.Dialog <| Dialog.UpdateAndClose <| Msg.CreatePayment data.name data.cost data.date data.frequency + Msg.Dialog + <| Dialog.UpdateAndClose + <| Msg.CreatePayment data.name data.cost data.date data.category data.frequency Nothing -> - Msg.Dialog <| Dialog.Update <| DialogMsg.AddPaymentMsg <| Form.Submit + Msg.Dialog <| Dialog.Update <| DialogMsg.AddPaymentMsg categories paymentCategories <| Form.Submit diff --git a/src/client/elm/Dialog/Model.elm b/src/client/elm/Dialog/Model.elm index a3901f9..d4fd484 100644 --- a/src/client/elm/Dialog/Model.elm +++ b/src/client/elm/Dialog/Model.elm @@ -16,14 +16,17 @@ import Model.Translations exposing (Translations) import Dialog.AddPayment.Model as AddPayment import Dialog.AddIncome.Model as AddIncome +import Dialog.AddCategory.Model as AddCategory type alias Model = { addPayment : Form String AddPayment.Model , addIncome : Form String AddIncome.Model + , addCategory : Form String AddCategory.Model } init : Model init = { addPayment = AddPayment.init , addIncome = AddIncome.init + , addCategory = AddCategory.init } diff --git a/src/client/elm/Dialog/Msg.elm b/src/client/elm/Dialog/Msg.elm index d504281..68ed146 100644 --- a/src/client/elm/Dialog/Msg.elm +++ b/src/client/elm/Dialog/Msg.elm @@ -4,7 +4,12 @@ module Dialog.Msg exposing import Form exposing (Form) +import Model.Category exposing (Categories) +import Model.PaymentCategory exposing (PaymentCategories) + type Msg = NoOp - | AddPaymentMsg Form.Msg + | Init String Msg + | AddPaymentMsg Categories PaymentCategories Form.Msg | AddIncomeMsg Form.Msg + | AddCategoryMsg Form.Msg diff --git a/src/client/elm/Dialog/Update.elm b/src/client/elm/Dialog/Update.elm index d69082d..3915548 100644 --- a/src/client/elm/Dialog/Update.elm +++ b/src/client/elm/Dialog/Update.elm @@ -2,10 +2,19 @@ module Dialog.Update exposing ( update ) +import Dom exposing (Id) import Form exposing (Form) +import Form.Field as Field +import Task -import Dialog.Msg as Dialog +import Dialog.AddCategory.Model as AddCategory +import Dialog.AddIncome.Model as AddIncome +import Dialog.AddPayment.Model as AddPayment import Dialog.Model as Dialog +import Dialog.Msg as Dialog + +import Model.Category exposing (Categories) +import Model.PaymentCategory as PaymentCategory exposing (PaymentCategories) update : Dialog.Msg -> Dialog.Model -> (Dialog.Model, Cmd Dialog.Msg) update msg model = @@ -16,16 +25,50 @@ update msg model = , Cmd.none ) - Dialog.AddPaymentMsg formMsg -> + Dialog.Init inputId dialogMsg -> + update dialogMsg model + |> Tuple.mapSecond (\cmd -> Cmd.batch [cmd, inputFocus inputId]) + + Dialog.AddPaymentMsg categories paymentCategories formMsg -> ( { model - | addPayment = Form.update formMsg model.addPayment + | addPayment = + Form.update (AddPayment.validation categories) formMsg model.addPayment + |> updateCategory categories paymentCategories formMsg } , Cmd.none ) Dialog.AddIncomeMsg formMsg -> ( { model - | addIncome = Form.update formMsg model.addIncome + | addIncome = Form.update AddIncome.validation formMsg model.addIncome } , Cmd.none ) + + Dialog.AddCategoryMsg formMsg -> + ( { model + | addCategory = Form.update AddCategory.validation formMsg model.addCategory + } + , Cmd.none + ) + +inputFocus : Id -> Cmd Dialog.Msg +inputFocus id = + Dom.focus id + |> Task.map (always Dialog.NoOp) + |> Task.onError (\_ -> Task.succeed Dialog.NoOp) + |> Task.perform (always Dialog.NoOp) + +updateCategory : Categories -> PaymentCategories -> Form.Msg -> (Form String AddPayment.Model -> Form String AddPayment.Model) +updateCategory categories paymentCategories formMsg = + case formMsg of + Form.Input "name" Form.Text (Field.String paymentName) -> + case PaymentCategory.search paymentName paymentCategories of + Just category -> + Form.update + (AddPayment.validation categories) + (Form.Input "category" Form.Text (Field.String <| toString category)) + Nothing -> + identity + _ -> + identity diff --git a/src/client/elm/Init.elm b/src/client/elm/Init.elm index 9c6fc3b..d87e870 100644 --- a/src/client/elm/Init.elm +++ b/src/client/elm/Init.elm @@ -5,7 +5,7 @@ module Init exposing import Time exposing (..) -import Json.Decode as Json exposing ((:=)) +import Json.Decode as Decode exposing (Decoder) import Model.Translations exposing (..) import Model.Conf exposing (..) @@ -20,11 +20,11 @@ type alias Init = , windowSize : Size } -decoder : Json.Decoder Init +decoder : Decoder Init decoder = - Json.object5 Init - ("time" := Json.float) - ("translations" := translationsDecoder) - ("conf" := confDecoder) - ("result" := initResultDecoder) - ("windowSize" := sizeDecoder) + Decode.map5 Init + (Decode.field "time" Decode.float) + (Decode.field "translations" translationsDecoder) + (Decode.field "conf" confDecoder) + (Decode.field "result" initResultDecoder) + (Decode.field "windowSize" sizeDecoder) diff --git a/src/client/elm/LoggedData.elm b/src/client/elm/LoggedData.elm index d4c31f1..9bb0a7f 100644 --- a/src/client/elm/LoggedData.elm +++ b/src/client/elm/LoggedData.elm @@ -13,6 +13,8 @@ import Model.Conf exposing (..) import Model.Payment exposing (Payments) import Model.User exposing (Users, UserId) import Model.Income exposing (Incomes) +import Model.Category exposing (Categories) +import Model.PaymentCategory exposing (PaymentCategories) import LoggedIn.Model as LoggedInModel @@ -24,6 +26,8 @@ type alias LoggedData = , me : UserId , payments : Payments , incomes : Incomes + , categories : Categories + , paymentCategories : PaymentCategories } build : Model -> LoggedInModel.Model -> LoggedData @@ -35,4 +39,6 @@ build model loggedIn = , me = loggedIn.me , payments = loggedIn.payments , incomes = loggedIn.incomes + , categories = loggedIn.categories + , paymentCategories = loggedIn.paymentCategories } diff --git a/src/client/elm/LoggedIn/Category/Model.elm b/src/client/elm/LoggedIn/Category/Model.elm new file mode 100644 index 0000000..7092fc4 --- /dev/null +++ b/src/client/elm/LoggedIn/Category/Model.elm @@ -0,0 +1,36 @@ +module LoggedIn.Category.Model exposing + ( Model + , AddCategory + , init + , initForm + , validation + ) + +import Date exposing (Date) + +import Form exposing (Form) +import Form.Validate as Validate exposing (Validation) +import Validation + +type alias Model = + { addCategory : Form String AddCategory + } + +type alias AddCategory = + { amount : Int + , date : Date + } + +init : Model +init = + { addCategory = initForm + } + +initForm : Form String AddCategory +initForm = Form.initial [] validation + +validation : Validation String AddCategory +validation = + Validate.map2 AddCategory + (Validate.field "amount" (Validate.int |> Validate.andThen (Validate.minInt 1))) + (Validate.field "date" Validation.date) diff --git a/src/client/elm/LoggedIn/Category/Msg.elm b/src/client/elm/LoggedIn/Category/Msg.elm new file mode 100644 index 0000000..3184297 --- /dev/null +++ b/src/client/elm/LoggedIn/Category/Msg.elm @@ -0,0 +1,9 @@ +module LoggedIn.Category.Msg exposing + ( Msg(..) + ) + +import Form exposing (Form) + +type Msg = + NoOp + | AddCategoryMsg Form.Msg diff --git a/src/client/elm/LoggedIn/Category/Table/View.elm b/src/client/elm/LoggedIn/Category/Table/View.elm new file mode 100644 index 0000000..fa7a7b1 --- /dev/null +++ b/src/client/elm/LoggedIn/Category/Table/View.elm @@ -0,0 +1,124 @@ +module LoggedIn.Category.Table.View exposing + ( view + ) + +import Dict exposing (..) +import Date exposing (Date) +import String exposing (append) + +import FontAwesome +import View.Color as Color + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) + +import Dialog +import Dialog.AddCategory.Model as AddCategory +import Dialog.AddCategory.View as AddCategory + +import Tooltip + +import Msg exposing (Msg) + +import LoggedData exposing (LoggedData) + +import LoggedIn.Msg as LoggedInMsg + +import LoggedIn.Category.Model as Category +import View.Date as Date +import LoggedIn.View.Format as Format + +import Model.User exposing (getUserName) +import Model.Category as Category exposing (CategoryId, Category) +import Model.PaymentCategory as PaymentCategory +import Model.Translations exposing (getMessage) + +view : LoggedData -> Category.Model -> Html Msg +view loggedData categoryModel = + let categories = + loggedData.categories + |> Dict.toList + |> List.sortBy (.name << Tuple.second) + in div + [ class "table" ] + [ div + [ class "lines" ] + ( headerLine loggedData :: List.map (paymentLine loggedData categoryModel) categories) + , if List.isEmpty (Dict.toList loggedData.categories) + then + div + [ class "emptyTableMsg" ] + [ text <| getMessage loggedData.translations "NoCategories" ] + else + text "" + ] + +headerLine : LoggedData -> Html Msg +headerLine loggedData = + div + [ class "header" ] + [ div [ class "cell name" ] [ text <| getMessage loggedData.translations "Name" ] + , div [ class "cell category" ] [ text <| getMessage loggedData.translations "Color" ] + , div [ class "cell" ] [] + , div [ class "cell" ] [] + , div [ class "cell" ] [] + ] + +paymentLine : LoggedData -> Category.Model -> (CategoryId, Category) -> Html Msg +paymentLine loggedData categoryModel (categoryId, category) = + div + [ class "row" ] + [ div + [ class "cell category" ] + [ text category.name ] + , div + [ class "cell category" ] + [ span + [ class "tag" + , style [("background-color", category.color)] + ] + [ text category.color ] + ] + , div + [ class "cell button" ] + [ let currentDate = Date.fromTime loggedData.currentTime + in AddCategory.button + loggedData + (AddCategory.initialClone loggedData.translations category) + "CloneCategory" + (FontAwesome.clone Color.chestnutRose 18) + (Just (getMessage loggedData.translations "Clone")) + ] + , div + [ class "cell button" ] + [ AddCategory.button + loggedData + (AddCategory.initialEdit loggedData.translations categoryId category) + "EditCategory" + (FontAwesome.pencil Color.chestnutRose 18) + (Just (getMessage loggedData.translations "Edit")) + ] + , div + [ class "cell button" ] + [ if PaymentCategory.isCategoryUnused categoryId loggedData.paymentCategories + then + let dialogConfig = + { className = "deleteCategoryDialog" + , title = getMessage loggedData.translations "ConfirmCategoryDelete" + , body = always <| text "" + , confirm = getMessage loggedData.translations "Confirm" + , confirmMsg = always <| Msg.Dialog <| Dialog.UpdateAndClose <| Msg.DeleteCategory categoryId + , undo = getMessage loggedData.translations "Undo" + } + in button + ( Tooltip.show Msg.Tooltip (getMessage loggedData.translations "Delete") + ++ [ onClick (Msg.Dialog <| Dialog.Open dialogConfig) ] + ) + [ FontAwesome.trash Color.chestnutRose 18 ] + else + span + ( Tooltip.show Msg.Tooltip (getMessage loggedData.translations "UsedCategory") ) + [ FontAwesome.trash Color.silver 18 ] + ] + ] diff --git a/src/client/elm/LoggedIn/Category/Update.elm b/src/client/elm/LoggedIn/Category/Update.elm new file mode 100644 index 0000000..1072ef0 --- /dev/null +++ b/src/client/elm/LoggedIn/Category/Update.elm @@ -0,0 +1,24 @@ +module LoggedIn.Category.Update exposing + ( update + ) + +import Form exposing (Form) + +import LoggedData exposing (LoggedData) + +import LoggedIn.Category.Model as Category +import LoggedIn.Category.Msg as Category + +update : LoggedData -> Category.Msg -> Category.Model -> (Category.Model, Cmd Category.Msg) +update loggedData msg model = + case msg of + + Category.NoOp -> + ( model + , Cmd.none + ) + + Category.AddCategoryMsg formMsg -> + ( { model | addCategory = Form.update Category.validation formMsg model.addCategory } + , Cmd.none + ) diff --git a/src/client/elm/LoggedIn/Category/View.elm b/src/client/elm/LoggedIn/Category/View.elm new file mode 100644 index 0000000..4e04fa2 --- /dev/null +++ b/src/client/elm/LoggedIn/Category/View.elm @@ -0,0 +1,35 @@ +module LoggedIn.Category.View exposing + ( view + ) + +import Html exposing (..) +import Html.Attributes exposing (..) + +import LoggedData exposing (LoggedData) + +import Msg exposing (Msg) + +import Dialog.AddCategory.Model as AddCategory +import Dialog.AddCategory.View as AddCategory + +import LoggedIn.Category.Model as Category +import LoggedIn.Category.Table.View as Table + +import Model.Translations exposing (getMessage, getParamMessage) + +view : LoggedData -> Category.Model -> Html Msg +view loggedData categoryModel = + div + [ class "categories" ] + [ div + [ class "titleButton withMargin" ] + [ h1 [] [ text <| getMessage loggedData.translations "Categories" ] + , AddCategory.button + loggedData + (AddCategory.initialAdd loggedData.translations) + "AddCategory" + (text (getMessage loggedData.translations "AddCategory")) + Nothing + ] + , Table.view loggedData categoryModel + ] diff --git a/src/client/elm/LoggedIn/Home/Header/View.elm b/src/client/elm/LoggedIn/Home/Header/View.elm index b67fb3b..3f8a320 100644 --- a/src/client/elm/LoggedIn/Home/Header/View.elm +++ b/src/client/elm/LoggedIn/Home/Header/View.elm @@ -5,7 +5,6 @@ module LoggedIn.Home.Header.View exposing import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (..) -import Html.App as Html import String import Dict import Date @@ -32,8 +31,6 @@ import LoggedIn.Home.View.ExceedingPayers as ExceedingPayers import LoggedIn.View.Format as Format import View.Plural exposing (plural) -import Utils.Tuple as Tuple - view : LoggedData -> Home.Model -> Payments -> Frequency -> Html Msg view loggedData { search } payments frequency = let currentDate = Date.fromTime loggedData.currentTime @@ -90,9 +87,9 @@ paymentsPartition loggedData payments = ", " ( loggedData.users |> Dict.toList - |> List.map (Tuple.mapFst (\userId -> Payment.totalPayments (always True) userId payments)) + |> List.map (Tuple.mapFirst (\userId -> Payment.totalPayments (always True) userId payments)) |> List.filter (\(sum, _) -> sum > 0) - |> List.sortBy fst + |> List.sortBy Tuple.first |> List.reverse |> List.map (\(sum, user) -> getParamMessage [ user.name, Format.price loggedData.conf sum ] loggedData.translations "By" diff --git a/src/client/elm/LoggedIn/Home/Model.elm b/src/client/elm/LoggedIn/Home/Model.elm index caedc29..ace1593 100644 --- a/src/client/elm/LoggedIn/Home/Model.elm +++ b/src/client/elm/LoggedIn/Home/Model.elm @@ -3,6 +3,7 @@ module LoggedIn.Home.Model exposing , Search , init , searchInitial + , validation ) import Form exposing (Form) @@ -26,14 +27,14 @@ type alias Search = init : Model init = { currentPage = 1 - , search = Form.initial (searchInitial Punctual) searchValidation + , search = Form.initial (searchInitial Punctual) validation } searchInitial : Frequency -> List (String, Field) -searchInitial frequency = [ ("frequency", Field.Radio (toString frequency)) ] +searchInitial frequency = [ ("frequency", Field.string (toString frequency)) ] -searchValidation : Validation String Search -searchValidation = - Validate.form2 Search - (Validate.get "name" (Validate.maybe Validate.string)) - (Validate.get "frequency" Payment.validateFrequency) +validation : Validation String Search +validation = + Validate.map2 Search + (Validate.field "name" (Validate.maybe Validate.string)) + (Validate.field "frequency" Payment.validateFrequency) diff --git a/src/client/elm/LoggedIn/Home/Update.elm b/src/client/elm/LoggedIn/Home/Update.elm index d1a3239..b0ce256 100644 --- a/src/client/elm/LoggedIn/Home/Update.elm +++ b/src/client/elm/LoggedIn/Home/Update.elm @@ -25,10 +25,10 @@ update loggedData msg model = Home.SearchMsg formMsg -> ( { model - | search = Form.update formMsg model.search + | search = Form.update Home.validation formMsg model.search , currentPage = case formMsg of - Form.Input "name" _ -> 1 + Form.Input "name" _ _ -> 1 _ -> model.currentPage } , Cmd.none diff --git a/src/client/elm/LoggedIn/Home/View.elm b/src/client/elm/LoggedIn/Home/View.elm index 0def64e..0b90e67 100644 --- a/src/client/elm/LoggedIn/Home/View.elm +++ b/src/client/elm/LoggedIn/Home/View.elm @@ -2,23 +2,22 @@ module LoggedIn.Home.View exposing ( view ) +import Date import Html exposing (..) import Html.Attributes exposing (..) -import Date import Form import Utils.Form as Form -import Msg exposing (Msg) - import LoggedData exposing (LoggedData) -import Model.Payment as Payment exposing (Frequency(..)) - -import LoggedIn.Home.Model as Home import LoggedIn.Home.Header.View as Header - -import LoggedIn.Home.View.Table as Table +import LoggedIn.Home.Model as Home +import LoggedIn.Home.Msg as HomeMsg import LoggedIn.Home.View.Paging as Paging +import LoggedIn.Home.View.Table as Table +import LoggedIn.Msg as LoggedInMsg +import Model.Payment as Payment exposing (Frequency(..)) +import Msg exposing (Msg) view : LoggedData -> Home.Model -> Html Msg view loggedData home = @@ -31,5 +30,9 @@ view loggedData home = [ class "home" ] [ Header.view loggedData home payments frequency , Table.view loggedData home payments frequency - , Paging.view home payments + , Paging.view + home.currentPage + (List.length payments) + Msg.NoOp + (Msg.UpdateLoggedIn << LoggedInMsg.HomeMsg << HomeMsg.UpdatePage) ] diff --git a/src/client/elm/LoggedIn/Home/View/Paging.elm b/src/client/elm/LoggedIn/Home/View/Paging.elm index 5bcb827..dffe061 100644 --- a/src/client/elm/LoggedIn/Home/View/Paging.elm +++ b/src/client/elm/LoggedIn/Home/View/Paging.elm @@ -10,31 +10,29 @@ import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (..) -import LoggedIn.Msg as LoggedInMsg - -import LoggedIn.Home.Msg as HomeMsg -import LoggedIn.Home.Model as HomeModel - -import Msg exposing (Msg) import LoggedData exposing (LoggedData) import Model.Payment as Payment exposing (Payments, perPage) showedPages : Int showedPages = 5 -view : HomeModel.Model -> Payments -> Html Msg -view homeModel payments = - let maxPage = ceiling (toFloat (List.length payments) / toFloat perPage) - pages = truncatePages homeModel.currentPage [1..maxPage] +view : Int -> Int -> msg -> (Int -> msg) -> Html msg +view currentPage payments noOp pageMsg = + let maxPage = ceiling (toFloat payments / toFloat perPage) + pages = truncatePages currentPage (List.range 1 maxPage) in if maxPage <= 1 then text "" else div [ class "pages" ] - ( [ firstPage homeModel, previousPage homeModel ] - ++ ( List.map (paymentsPage homeModel) pages) - ++ [ nextPage homeModel maxPage, lastPage homeModel maxPage ] + ( [ firstPage currentPage pageMsg + , previousPage currentPage noOp pageMsg + ] + ++ ( List.map (paymentsPage currentPage noOp pageMsg) pages) + ++ [ nextPage currentPage maxPage noOp pageMsg + , lastPage currentPage maxPage pageMsg + ] ) truncatePages : Int -> List Int -> List Int @@ -44,57 +42,57 @@ truncatePages currentPage pages = showedRightPages = floor ((toFloat showedPages - 1) / 2) truncatedPages = if currentPage <= showedLeftPages then - [1..showedPages] + (List.range 1 showedPages) else if currentPage > totalPages - showedRightPages then - [(totalPages - showedPages + 1)..totalPages] + (List.range (totalPages - showedPages + 1) totalPages) else - [(currentPage - showedLeftPages)..(currentPage + showedRightPages)] + (List.range (currentPage - showedLeftPages) (currentPage + showedRightPages)) in List.filter (flip List.member pages) truncatedPages -firstPage : HomeModel.Model -> Html Msg -firstPage homeModel = +firstPage : Int -> (Int -> msg) -> Html msg +firstPage currentPage pageMsg = button [ classList [ ("page", True) - , ("disable", homeModel.currentPage <= 1) + , ("disable", currentPage <= 1) ] - , onClick (Msg.UpdateLoggedIn << LoggedInMsg.HomeMsg << HomeMsg.UpdatePage <| 1) + , onClick (pageMsg 1) ] [ FontAwesome.fast_backward grey 13 ] -previousPage : HomeModel.Model -> Html Msg -previousPage homeModel = +previousPage : Int -> msg -> (Int -> msg) -> Html msg +previousPage currentPage noOp pageMsg = button [ class "page" , onClick <| - if homeModel.currentPage > 1 - then (Msg.UpdateLoggedIn << LoggedInMsg.HomeMsg << HomeMsg.UpdatePage <| homeModel.currentPage - 1) - else Msg.NoOp + if currentPage > 1 + then (pageMsg <| currentPage - 1) + else noOp ] [ FontAwesome.backward grey 13 ] -nextPage : HomeModel.Model -> Int -> Html Msg -nextPage homeModel maxPage = +nextPage : Int -> Int -> msg -> (Int -> msg) -> Html msg +nextPage currentPage maxPage noOp pageMsg = button [ class "page" , onClick <| - if homeModel.currentPage < maxPage - then (Msg.UpdateLoggedIn << LoggedInMsg.HomeMsg << HomeMsg.UpdatePage <| homeModel.currentPage + 1) - else Msg.NoOp + if currentPage < maxPage + then (pageMsg <| currentPage + 1) + else noOp ] [ FontAwesome.forward grey 13 ] -lastPage : HomeModel.Model -> Int -> Html Msg -lastPage homeModel maxPage = +lastPage : Int -> Int -> (Int -> msg) -> Html msg +lastPage currentPage maxPage pageMsg = button [ class "page" - , onClick (Msg.UpdateLoggedIn << LoggedInMsg.HomeMsg << HomeMsg.UpdatePage <| maxPage) + , onClick (pageMsg maxPage) ] [ FontAwesome.fast_forward grey 13 ] -paymentsPage : HomeModel.Model -> Int -> Html Msg -paymentsPage homeModel page = - let onCurrentPage = page == homeModel.currentPage +paymentsPage : Int -> msg -> (Int -> msg) -> Int -> Html msg +paymentsPage currentPage noOp pageMsg page = + let onCurrentPage = page == currentPage in button [ classList [ ("page", True) @@ -102,8 +100,8 @@ paymentsPage homeModel page = ] , onClick <| if onCurrentPage - then Msg.NoOp - else Msg.UpdateLoggedIn << LoggedInMsg.HomeMsg << HomeMsg.UpdatePage <| page + then noOp + else pageMsg page ] [ text (toString page) ] diff --git a/src/client/elm/LoggedIn/Home/View/Table.elm b/src/client/elm/LoggedIn/Home/View/Table.elm index 6423bf9..8828488 100644 --- a/src/client/elm/LoggedIn/Home/View/Table.elm +++ b/src/client/elm/LoggedIn/Home/View/Table.elm @@ -2,8 +2,8 @@ module LoggedIn.Home.View.Table exposing ( view ) -import Dict exposing (..) import Date exposing (Date) +import Dict exposing (..) import String exposing (append) import FontAwesome @@ -26,12 +26,13 @@ import LoggedData exposing (LoggedData) import LoggedIn.Msg as LoggedInMsg import LoggedIn.Home.Model as Home -import View.Date as Date import LoggedIn.View.Format as Format +import View.Date as Date -import Model.User exposing (getUserName) import Model.Payment as Payment exposing (..) +import Model.PaymentCategory as PaymentCategory import Model.Translations exposing (getMessage) +import Model.User exposing (getUserName) view : LoggedData -> Home.Model -> Payments -> Frequency -> Html Msg view loggedData homeModel payments frequency = @@ -60,6 +61,7 @@ headerLine loggedData frequency = [ div [ class "cell category" ] [ text <| getMessage loggedData.translations "Name" ] , div [ class "cell cost" ] [ text <| getMessage loggedData.translations "Cost" ] , div [ class "cell user" ] [ text <| getMessage loggedData.translations "Payer" ] + , div [ class "cell user" ] [ text <| getMessage loggedData.translations "PaymentCategory" ] , case frequency of Punctual -> div [ class "cell date" ] [ text <| getMessage loggedData.translations "Date" ] Monthly -> text "" @@ -72,7 +74,7 @@ paymentLine : LoggedData -> Home.Model -> Frequency -> Payment -> Html Msg paymentLine loggedData homeModel frequency payment = div [ class "row" ] - [ div [ class "cell category" ] [ text payment.name ] + [ div [ class "cell name" ] [ text payment.name ] , div [ classList [ ("cell cost", True) @@ -87,6 +89,22 @@ paymentLine loggedData homeModel frequency payment = |> Maybe.withDefault "−" |> text ] + , div + [ class "cell category" ] + ( let mbCategory = + PaymentCategory.search payment.name loggedData.paymentCategories + |> Maybe.andThen (\category -> Dict.get category loggedData.categories) + in case mbCategory of + Just category -> + [ span + [ class "tag" + , style [("background-color", category.color)] + ] + [ text category.name ] + ] + Nothing -> + [] + ) , case frequency of Punctual -> div @@ -103,9 +121,10 @@ paymentLine loggedData homeModel frequency payment = , div [ class "cell button" ] [ let currentDate = Date.fromTime loggedData.currentTime + category = PaymentCategory.search payment.name loggedData.paymentCategories in AddPayment.button loggedData - (AddPayment.initialClone loggedData.translations currentDate payment) + (AddPayment.initialClone loggedData.translations currentDate category payment) "ClonePayment" (FontAwesome.clone Color.chestnutRose 18) (Just (getMessage loggedData.translations "Clone")) @@ -116,12 +135,13 @@ paymentLine loggedData homeModel frequency payment = then text "" else - AddPayment.button - loggedData - (AddPayment.initialEdit loggedData.translations payment) - "EditPayment" - (FontAwesome.pencil Color.chestnutRose 18) - (Just (getMessage loggedData.translations "Edit")) + let category = PaymentCategory.search payment.name loggedData.paymentCategories + in AddPayment.button + loggedData + (AddPayment.initialEdit loggedData.translations category payment) + "EditPayment" + (FontAwesome.pencil Color.chestnutRose 18) + (Just (getMessage loggedData.translations "Edit")) ] , div [ class "cell button" ] diff --git a/src/client/elm/LoggedIn/Income/Model.elm b/src/client/elm/LoggedIn/Income/Model.elm index cf1bf57..7d852b9 100644 --- a/src/client/elm/LoggedIn/Income/Model.elm +++ b/src/client/elm/LoggedIn/Income/Model.elm @@ -3,12 +3,13 @@ module LoggedIn.Income.Model exposing , AddIncome , init , initForm + , validation ) import Date exposing (Date) import Form exposing (Form) -import Form.Validate as Validate exposing (..) +import Form.Validate as Validate exposing (Validation) import Validation type alias Model = @@ -26,10 +27,10 @@ init = } initForm : Form String AddIncome -initForm = Form.initial [] validate +initForm = Form.initial [] validation -validate : Validation String AddIncome -validate = - form2 AddIncome - (get "amount" (int `andThen` (minInt 1))) - (get "date" Validation.date) +validation : Validation String AddIncome +validation = + Validate.map2 AddIncome + (Validate.field "amount" (Validate.int |> Validate.andThen (Validate.minInt 1))) + (Validate.field "date" Validation.date) diff --git a/src/client/elm/LoggedIn/Income/Update.elm b/src/client/elm/LoggedIn/Income/Update.elm index ec6a0c1..0023c76 100644 --- a/src/client/elm/LoggedIn/Income/Update.elm +++ b/src/client/elm/LoggedIn/Income/Update.elm @@ -6,19 +6,19 @@ import Form exposing (Form) import LoggedData exposing (LoggedData) -import LoggedIn.Income.Model as IncomeModel -import LoggedIn.Income.Msg as IncomeMsg +import LoggedIn.Income.Model as Income +import LoggedIn.Income.Msg as Income -update : LoggedData -> IncomeMsg.Msg -> IncomeModel.Model -> (IncomeModel.Model, Cmd IncomeMsg.Msg) +update : LoggedData -> Income.Msg -> Income.Model -> (Income.Model, Cmd Income.Msg) update loggedData msg model = case msg of - IncomeMsg.NoOp -> + Income.NoOp -> ( model , Cmd.none ) - IncomeMsg.AddIncomeMsg formMsg -> - ( { model | addIncome = Form.update formMsg model.addIncome } + Income.AddIncomeMsg formMsg -> + ( { model | addIncome = Form.update Income.validation formMsg model.addIncome } , Cmd.none ) diff --git a/src/client/elm/LoggedIn/Income/View.elm b/src/client/elm/LoggedIn/Income/View.elm index 2c5bcaf..00a1646 100644 --- a/src/client/elm/LoggedIn/Income/View.elm +++ b/src/client/elm/LoggedIn/Income/View.elm @@ -12,7 +12,6 @@ import FontAwesome import Html exposing (..) import Html.Events exposing (..) import Html.Attributes exposing (..) -import Html.App as Html import Form exposing (Form) import View.Form as Form @@ -45,19 +44,21 @@ view : LoggedData -> Income.Model -> Html Msg view loggedData incomeModel = div [ class "income" ] - [ case useIncomesFrom loggedData.users loggedData.incomes loggedData.payments of - Just since -> cumulativeIncomesView loggedData since - Nothing -> text "" - , div - [ class "textual monthlyNetIncomes" ] - [ h1 [] [ text <| getMessage loggedData.translations "MonthlyNetIncomes" ] - , AddIncome.button - "addIncome" - loggedData - (AddIncome.initialAdd loggedData.translations (Date.fromTime loggedData.currentTime)) - "AddIncome" - (text (getMessage loggedData.translations "AddIncome")) - Nothing + [ div + [ class "withMargin" ] + [ case useIncomesFrom loggedData.users loggedData.incomes loggedData.payments of + Just since -> cumulativeIncomesView loggedData since + Nothing -> text "" + , div + [ class "titleButton" ] + [ h1 [] [ text <| getMessage loggedData.translations "MonthlyNetIncomes" ] + , AddIncome.button + loggedData + (AddIncome.initialAdd loggedData.translations (Date.fromTime loggedData.currentTime)) + "AddIncome" + (text (getMessage loggedData.translations "AddIncome")) + Nothing + ] ] , Table.view loggedData incomeModel ] @@ -66,7 +67,7 @@ cumulativeIncomesView : LoggedData -> Time -> Html Msg cumulativeIncomesView loggedData since = let longDate = Date.longView (Date.fromTime since) loggedData.translations in div - [ class "textual" ] + [] [ h1 [] [ text <| getParamMessage [longDate] loggedData.translations "CumulativeIncomesSince" ] , ul [] @@ -74,7 +75,7 @@ cumulativeIncomesView loggedData since = |> List.map (\(userId, user) -> (user.name, userCumulativeIncomeSince loggedData.currentTime since loggedData.incomes userId) ) - |> List.sortBy snd + |> List.sortBy Tuple.second |> List.map (\(userName, cumulativeIncome) -> li [] diff --git a/src/client/elm/LoggedIn/Income/View/Table.elm b/src/client/elm/LoggedIn/Income/View/Table.elm index dcf6d78..aa5e392 100644 --- a/src/client/elm/LoggedIn/Income/View/Table.elm +++ b/src/client/elm/LoggedIn/Income/View/Table.elm @@ -38,7 +38,7 @@ view loggedData incomeModel = let incomes = loggedData.incomes |> Dict.toList - |> List.sortBy (.time << snd) + |> List.sortBy (.time << Tuple.second) |> List.reverse in div [ class "table" ] @@ -49,7 +49,7 @@ view loggedData incomeModel = then div [ class "emptyTableMsg" ] - [ text <| getMessage loggedData.translations "NoPayment" ] + [ text <| getMessage loggedData.translations "NoIncome" ] else text "" ] @@ -87,7 +87,6 @@ paymentLine loggedData incomeModel (incomeId, income) = [ class "cell button" ] [ let currentDate = Date.fromTime loggedData.currentTime in AddIncome.button - "" loggedData (AddIncome.initialClone loggedData.translations currentDate income) "CloneIncome" @@ -101,7 +100,6 @@ paymentLine loggedData incomeModel (incomeId, income) = text "" else AddIncome.button - "" loggedData (AddIncome.initialEdit loggedData.translations incomeId income) "EditIncome" diff --git a/src/client/elm/LoggedIn/Model.elm b/src/client/elm/LoggedIn/Model.elm index 11386d5..6bcb0b2 100644 --- a/src/client/elm/LoggedIn/Model.elm +++ b/src/client/elm/LoggedIn/Model.elm @@ -9,25 +9,34 @@ import Model.Init exposing (..) import Model.Payment exposing (Payments) import Model.User exposing (Users, UserId) import Model.Income exposing (Incomes) +import Model.Category exposing (Categories) +import Model.PaymentCategory exposing (PaymentCategories) -import LoggedIn.Home.Model as HomeModel -import LoggedIn.Income.Model as IncomeModel +import LoggedIn.Home.Model as Home +import LoggedIn.Income.Model as Income +import LoggedIn.Category.Model as Categories type alias Model = - { home : HomeModel.Model - , income : IncomeModel.Model + { home : Home.Model + , income : Income.Model + , category : Categories.Model , users : Users , me : UserId , payments : Payments , incomes : Incomes + , categories : Categories + , paymentCategories : PaymentCategories } init : Init -> Model init initData = - { home = HomeModel.init - , income = IncomeModel.init + { home = Home.init + , income = Income.init + , category = Categories.init , users = initData.users , me = initData.me , payments = initData.payments , incomes = initData.incomes + , categories = initData.categories + , paymentCategories = initData.paymentCategories } diff --git a/src/client/elm/LoggedIn/Msg.elm b/src/client/elm/LoggedIn/Msg.elm index ff275e2..a1379a6 100644 --- a/src/client/elm/LoggedIn/Msg.elm +++ b/src/client/elm/LoggedIn/Msg.elm @@ -6,17 +6,23 @@ import Date exposing (Date) import Model.Payment exposing (PaymentId, Frequency) import Model.Income exposing (IncomeId) +import Model.Category exposing (CategoryId) -import LoggedIn.Home.Msg as HomeMsg -import LoggedIn.Income.Msg as IncomeMsg +import LoggedIn.Home.Msg as Home +import LoggedIn.Income.Msg as Income +import LoggedIn.Category.Msg as Categories type Msg = NoOp - | HomeMsg HomeMsg.Msg - | IncomeMsg IncomeMsg.Msg - | ValidateCreatePayment PaymentId String Int Date Frequency - | ValidateEditPayment PaymentId String Int Date Frequency + | HomeMsg Home.Msg + | IncomeMsg Income.Msg + | CategoriesMsg Categories.Msg + | ValidateCreatePayment PaymentId String Int Date CategoryId Frequency + | ValidateEditPayment PaymentId String Int Date CategoryId Frequency | ValidateDeletePayment PaymentId | ValidateCreateIncome IncomeId Int Date | ValidateEditIncome IncomeId Int Date | ValidateDeleteIncome IncomeId + | ValidateCreateCategory CategoryId String String + | ValidateEditCategory CategoryId String String + | ValidateDeleteCategory CategoryId diff --git a/src/client/elm/LoggedIn/Stat/View.elm b/src/client/elm/LoggedIn/Stat/View.elm index 946005a..f57316a 100644 --- a/src/client/elm/LoggedIn/Stat/View.elm +++ b/src/client/elm/LoggedIn/Stat/View.elm @@ -26,7 +26,7 @@ view loggedData = let paymentsByMonth = Payment.groupAndSortByMonth (Payment.punctual loggedData.payments) monthPaymentMean = getMonthPaymentMean loggedData paymentsByMonth in div - [ class "stat textual" ] + [ class "stat withMargin" ] [ h1 [] [ text (getParamMessage [ Format.price loggedData.conf monthPaymentMean ] loggedData.translations "ByMonthsAndMean") ] , ul [] @@ -40,7 +40,7 @@ getMonthPaymentMean loggedData paymentsByMonth = let currentDate = Date.fromTime loggedData.currentTime in not (Date.month currentDate == month && Date.year currentDate == year) ) - |> List.map (List.sum << List.map .cost << snd) + |> List.map (List.sum << List.map .cost << Tuple.second) |> List.mean monthDetail : LoggedData -> ((Month, Int), Payments) -> Html Msg diff --git a/src/client/elm/LoggedIn/Update.elm b/src/client/elm/LoggedIn/Update.elm index 06cd623..9e6d6ee 100644 --- a/src/client/elm/LoggedIn/Update.elm +++ b/src/client/elm/LoggedIn/Update.elm @@ -15,6 +15,8 @@ import Form import Model exposing (Model) import Model.Payment as Payment exposing (Payment, Frequency(..)) import Model.Income as Income exposing (Income) +import Model.Category exposing (Category) +import Model.PaymentCategory as PaymentCategory import Server import LoggedData @@ -22,16 +24,16 @@ import LoggedData import LoggedIn.Msg as LoggedInMsg import LoggedIn.Model as LoggedInModel -import LoggedIn.Home.Msg as HomeMsg -import LoggedIn.Home.Update as HomeUpdate -import LoggedIn.Home.Model as HomeModel +import LoggedIn.Home.Msg as Home +import LoggedIn.Home.Update as Home +import LoggedIn.Home.Model as Home -import LoggedIn.Income.Msg as IncomeMsg -import LoggedIn.Income.Update as IncomeUpdate +import LoggedIn.Income.Update as Income +import LoggedIn.Income.Model as Income -import LoggedIn.Income.Model as IncomeModel +import LoggedIn.Category.Update as Categories +import LoggedIn.Category.Model as Categories -import Utils.Tuple as Tuple import Utils.Cmd exposing ((:>)) update : Model -> LoggedInMsg.Msg -> LoggedInModel.Model -> (LoggedInModel.Model, Cmd LoggedInMsg.Msg) @@ -45,32 +47,51 @@ update model msg loggedIn = ) LoggedInMsg.HomeMsg homeMsg -> - case HomeUpdate.update loggedData homeMsg loggedIn.home of + case Home.update loggedData homeMsg loggedIn.home of (home, effects) -> ( { loggedIn | home = home } , Cmd.map LoggedInMsg.HomeMsg effects ) LoggedInMsg.IncomeMsg incomeMsg -> - case IncomeUpdate.update loggedData incomeMsg loggedIn.income of + case Income.update loggedData incomeMsg loggedIn.income of (income, cmd) -> ( { loggedIn | income = income } , Cmd.map LoggedInMsg.IncomeMsg cmd ) - LoggedInMsg.ValidateCreatePayment paymentId name cost date frequency -> - update model (LoggedInMsg.HomeMsg <| HomeMsg.SearchMsg (Form.Reset (HomeModel.searchInitial frequency))) loggedIn - :> update model (LoggedInMsg.HomeMsg <| HomeMsg.UpdatePage 1) + LoggedInMsg.CategoriesMsg categoriesMsg -> + case Categories.update loggedData categoriesMsg loggedIn.category of + (category, cmd) -> + ( { loggedIn | category = category } + , Cmd.map LoggedInMsg.CategoriesMsg cmd + ) + + LoggedInMsg.ValidateCreatePayment paymentId name cost date category frequency -> + update model (LoggedInMsg.HomeMsg <| Home.SearchMsg (Form.Reset (Home.searchInitial frequency))) loggedIn + :> update model (LoggedInMsg.HomeMsg <| Home.UpdatePage 1) :> (\loggedIn -> let newPayment = Payment paymentId name cost date loggedIn.me frequency - in ( { loggedIn | payments = newPayment :: loggedIn.payments } + in ( { loggedIn + | payments = newPayment :: loggedIn.payments + , paymentCategories = PaymentCategory.set name category loggedIn.paymentCategories + } , Cmd.none ) ) - LoggedInMsg.ValidateEditPayment paymentId name cost date frequency -> + LoggedInMsg.ValidateEditPayment paymentId name cost date category frequency -> let updatedPayment = Payment paymentId name cost date loggedIn.me frequency - in ( { loggedIn | payments = Payment.edit updatedPayment loggedIn.payments } + mbOldPayment = Payment.find paymentId loggedIn.payments + in ( { loggedIn + | payments = Payment.edit updatedPayment loggedIn.payments + , paymentCategories = + case mbOldPayment of + Just oldPayment -> + PaymentCategory.update oldPayment.name name category loggedIn.paymentCategories + Nothing -> + loggedData.paymentCategories + } , Cmd.none ) @@ -86,7 +107,7 @@ update model msg loggedIn = ) in if switchToPunctual then - update model (LoggedInMsg.HomeMsg <| HomeMsg.SearchMsg (Form.Reset (HomeModel.searchInitial Punctual))) loggedIn + update model (LoggedInMsg.HomeMsg <| Home.SearchMsg (Form.Reset (Home.searchInitial Punctual))) loggedIn :> (\loggedIn -> ( { loggedIn | payments = payments } , Cmd.none @@ -99,20 +120,12 @@ update model msg loggedIn = LoggedInMsg.ValidateCreateIncome incomeId amount date -> let newIncome = { userId = loggedIn.me, amount = amount, time = Date.toTime date } - loggedInIncome = loggedIn.income - in ( { loggedIn - | incomes = Dict.insert incomeId newIncome loggedIn.incomes - , income = { loggedInIncome | addIncome = IncomeModel.initForm } - } + in ( { loggedIn | incomes = Dict.insert incomeId newIncome loggedIn.incomes } , Cmd.none ) LoggedInMsg.ValidateEditIncome incomeId amount date -> - let updatedIncome = Income loggedIn.me (Date.toTime date) amount - updateIncome mbIncome = - case mbIncome of - Just _ -> Just updatedIncome - Nothing -> Just updatedIncome + let updateIncome _ = Just <| Income loggedIn.me (Date.toTime date) amount in ( { loggedIn | incomes = Dict.update incomeId updateIncome loggedIn.incomes } , Cmd.none ) @@ -121,3 +134,18 @@ update model msg loggedIn = ( { loggedIn | incomes = Dict.remove incomeId loggedIn.incomes } , Cmd.none ) + + LoggedInMsg.ValidateCreateCategory categoryId name color -> + let newCategory = { name = name, color = color } + in ( { loggedIn | categories = Dict.insert categoryId newCategory loggedIn.categories } + , Cmd.none + ) + + LoggedInMsg.ValidateEditCategory categoryId name color -> + let updateCategory _ = Just <| Category name color + in ( { loggedIn | categories = Dict.update categoryId updateCategory loggedIn.categories } , Cmd.none) + + LoggedInMsg.ValidateDeleteCategory categoryId -> + ( { loggedIn | categories = Dict.remove categoryId loggedIn.categories } + , Cmd.none + ) diff --git a/src/client/elm/LoggedIn/View.elm b/src/client/elm/LoggedIn/View.elm index a1fa3f0..2e42a73 100644 --- a/src/client/elm/LoggedIn/View.elm +++ b/src/client/elm/LoggedIn/View.elm @@ -9,13 +9,15 @@ import Page import Msg exposing (Msg) import Model exposing (Model) +import Model.Translations exposing (getMessage) import LoggedData import LoggedIn.Model as LoggedInModel -import LoggedIn.Home.View as HomeView -import LoggedIn.Income.View as UserView -import LoggedIn.Stat.View as StatView +import LoggedIn.Home.View as Home +import LoggedIn.Income.View as Income +import LoggedIn.Category.View as Categories +import LoggedIn.Stat.View as Stat view : Model -> LoggedInModel.Model -> Html Msg view model loggedIn = @@ -23,7 +25,9 @@ view model loggedIn = [ class "loggedIn" ] [ let loggedData = LoggedData.build model loggedIn in case model.page of - Page.Home -> HomeView.view loggedData loggedIn.home - Page.Income -> UserView.view loggedData loggedIn.income - Page.Statistics -> StatView.view loggedData + Page.Home -> Home.view loggedData loggedIn.home + Page.Income -> Income.view loggedData loggedIn.income + Page.Categories -> Categories.view loggedData loggedIn.category + Page.Statistics -> Stat.view loggedData + Page.NotFound -> div [] [ text (getMessage model.translations "PageNotFound") ] ] diff --git a/src/client/elm/Main.elm b/src/client/elm/Main.elm index fa1415d..9674b66 100644 --- a/src/client/elm/Main.elm +++ b/src/client/elm/Main.elm @@ -4,20 +4,19 @@ module Main exposing import Navigation import Time -import Msg +import Msg exposing (Msg(UpdatePage)) import Model exposing (init) -import Update exposing (update, urlUpdate) +import Update exposing (update) import View exposing (view) import Page import Tooltip main = - Navigation.programWithFlags (Navigation.makeParser Page.fromHash) + Navigation.programWithFlags (UpdatePage << Page.fromLocation) { init = init , view = view , update = update - , urlUpdate = urlUpdate , subscriptions = (\model -> Sub.batch [ Time.every 1000 Msg.UpdateTime diff --git a/src/client/elm/Model.elm b/src/client/elm/Model.elm index a1d2bff..5167e42 100644 --- a/src/client/elm/Model.elm +++ b/src/client/elm/Model.elm @@ -4,9 +4,11 @@ module Model exposing ) import Time exposing (Time) -import Json.Decode as Json +import Json.Decode as Decode -import Html.App as Html +import Navigation exposing (Location) + +import Html as Html import Page exposing (Page) import Init as Init exposing (Init) @@ -38,14 +40,10 @@ type alias Model = , tooltip : Tooltip.Model } -init : Json.Value -> Result String Page -> (Model, Cmd Msg) -init payload result = - let page = - case result of - Err _ -> Page.Home - Ok page -> page - model = - case Json.decodeValue Init.decoder payload of +init : Decode.Value -> Location -> (Model, Cmd Msg) +init payload location = + let model = + case Decode.decodeValue Init.decoder payload of Ok { time, translations, conf, result, windowSize } -> { view = case result of @@ -58,7 +56,7 @@ init payload result = , currentTime = time , translations = translations , conf = conf - , page = page + , page = Page.fromLocation location , errors = [] , dialog = Dialog.init DialogModel.init Msg.Dialog , tooltip = Tooltip.init windowSize.width windowSize.height @@ -68,7 +66,7 @@ init payload result = , currentTime = 0 , translations = [] , conf = { currency = "" } - , page = page + , page = Page.fromLocation location , errors = [ error ] , dialog = Dialog.init DialogModel.init Msg.Dialog , tooltip = Tooltip.init 0 0 diff --git a/src/client/elm/Model/Category.elm b/src/client/elm/Model/Category.elm new file mode 100644 index 0000000..8b653a7 --- /dev/null +++ b/src/client/elm/Model/Category.elm @@ -0,0 +1,35 @@ +module Model.Category exposing + ( Categories + , Category + , CategoryId + , categoriesDecoder + , categoryIdDecoder + , empty + ) + +import Json.Decode as Decode exposing (Decoder) +import Utils.Json as Json +import Dict exposing (Dict) + +type alias Categories = Dict CategoryId Category + +type alias CategoryId = Int + +type alias Category = + { name : String + , color : String + } + +categoriesDecoder : Decoder Categories +categoriesDecoder = + Json.dictDecoder (Decode.field "id" categoryIdDecoder) <| + Decode.map2 + Category + (Decode.field "name" Decode.string) + (Decode.field "color" Decode.string) + +categoryIdDecoder : Decoder CategoryId +categoryIdDecoder = Decode.int + +empty : Categories +empty = Dict.empty diff --git a/src/client/elm/Model/Conf.elm b/src/client/elm/Model/Conf.elm index ec04622..308fa04 100644 --- a/src/client/elm/Model/Conf.elm +++ b/src/client/elm/Model/Conf.elm @@ -3,11 +3,11 @@ module Model.Conf exposing , confDecoder ) -import Json.Decode exposing (..) +import Json.Decode as Decode exposing (Decoder) type alias Conf = { currency : String } confDecoder : Decoder Conf -confDecoder = object1 Conf ("currency" := string) +confDecoder = Decode.map Conf (Decode.field "currency" Decode.string) diff --git a/src/client/elm/Model/Date.elm b/src/client/elm/Model/Date.elm index f3c9b91..bfba02f 100644 --- a/src/client/elm/Model/Date.elm +++ b/src/client/elm/Model/Date.elm @@ -4,12 +4,12 @@ module Model.Date exposing ) import Date as Date exposing (Date) +import Json.Decode as Decode exposing (Decoder) +import Json.Decode.Extra as Decode import Time exposing (Time) -import Json.Decode as Json exposing (..) - timeDecoder : Decoder Time -timeDecoder = Json.map Date.toTime dateDecoder +timeDecoder = Decode.map Date.toTime dateDecoder dateDecoder : Decoder Date -dateDecoder = customDecoder string Date.fromString +dateDecoder = Decode.string |> Decode.andThen (Date.fromString >> Decode.fromResult) diff --git a/src/client/elm/Model/Income.elm b/src/client/elm/Model/Income.elm index a5ca34b..34578c6 100644 --- a/src/client/elm/Model/Income.elm +++ b/src/client/elm/Model/Income.elm @@ -9,7 +9,8 @@ module Model.Income exposing , cumulativeIncomesSince ) -import Json.Decode as Json exposing ((:=)) +import Json.Decode as Decode exposing (Decoder) +import Utils.Json as Json import Time exposing (Time, hour) import List exposing (..) import Dict exposing (Dict) @@ -17,7 +18,7 @@ import Dict exposing (Dict) import Model.Date exposing (timeDecoder) import Model.User exposing (UserId, userIdDecoder) -import Utils.Maybe exposing (isJust, catMaybes, maybeToList) +import Utils.Maybe as Maybe type alias Incomes = Dict IncomeId Income @@ -29,31 +30,23 @@ type alias Income = , amount : Int } -incomesDecoder : Json.Decoder Incomes -incomesDecoder = Json.map Dict.fromList (Json.list incomeWithIdDecoder) +incomesDecoder : Decoder Incomes +incomesDecoder = + Json.dictDecoder (Decode.field "id" incomeIdDecoder) <| + Decode.map3 Income + (Decode.field "userId" userIdDecoder) + (Decode.field "date" timeDecoder) + (Decode.field "amount" Decode.int) -incomeWithIdDecoder : Json.Decoder (IncomeId, Income) -incomeWithIdDecoder = - Json.object2 (,) - ("id" := incomeIdDecoder) - incomeDecoder - -incomeIdDecoder : Json.Decoder IncomeId -incomeIdDecoder = Json.int - -incomeDecoder : Json.Decoder Income -incomeDecoder = - Json.object3 Income - ("userId" := userIdDecoder) - ("date" := timeDecoder) - ("amount" := Json.int) +incomeIdDecoder : Decoder IncomeId +incomeIdDecoder = Decode.int incomeDefinedForAll : List UserId -> Incomes -> Maybe Time incomeDefinedForAll userIds incomes = let userIncomes = List.map (\userId -> List.filter ((==) userId << .userId) << Dict.values <| incomes) userIds firstIncomes = map (head << sortBy .time) userIncomes - in if all isJust firstIncomes - then head << reverse << List.sort << map .time << catMaybes <| firstIncomes + in if all Maybe.isJust firstIncomes + then head << reverse << List.sort << map .time << Maybe.cat <| firstIncomes else Nothing userCumulativeIncomeSince : Time -> Time -> Incomes -> UserId -> Int @@ -71,7 +64,7 @@ getOrderedIncomesSince : Time -> List Income -> List Income getOrderedIncomesSince time incomes = let mbStarterIncome = getIncomeAt time incomes orderedIncomesSince = filter (\income -> income.time >= time) incomes - in (maybeToList mbStarterIncome) ++ orderedIncomesSince + in (Maybe.toList mbStarterIncome) ++ orderedIncomesSince getIncomeAt : Time -> List Income -> Maybe Income getIncomeAt time incomes = diff --git a/src/client/elm/Model/Init.elm b/src/client/elm/Model/Init.elm index 3a86dba..db7069f 100644 --- a/src/client/elm/Model/Init.elm +++ b/src/client/elm/Model/Init.elm @@ -3,23 +3,29 @@ module Model.Init exposing , initDecoder ) -import Json.Decode as Json exposing ((:=)) +import Json.Decode as Decode exposing (Decoder) import Model.Payment exposing (Payments, paymentsDecoder) -import Model.Income exposing (Incomes, incomesDecoder) import Model.User exposing (Users, UserId, usersDecoder, userIdDecoder) +import Model.Income exposing (Incomes, incomesDecoder) +import Model.Category exposing (Categories, categoriesDecoder) +import Model.PaymentCategory exposing (PaymentCategories, paymentCategoriesDecoder) type alias Init = { users : Users , me : UserId , payments : Payments , incomes : Incomes + , categories : Categories + , paymentCategories : PaymentCategories } -initDecoder : Json.Decoder Init +initDecoder : Decoder Init initDecoder = - Json.object4 Init - ("users" := usersDecoder) - ("me" := userIdDecoder) - ("payments" := paymentsDecoder) - ("incomes" := incomesDecoder) + Decode.map6 Init + (Decode.field "users" usersDecoder) + (Decode.field "me" userIdDecoder) + (Decode.field "payments" paymentsDecoder) + (Decode.field "incomes" incomesDecoder) + (Decode.field "categories" categoriesDecoder) + (Decode.field "paymentCategories" paymentCategoriesDecoder) diff --git a/src/client/elm/Model/InitResult.elm b/src/client/elm/Model/InitResult.elm index c8da533..7ce0be2 100644 --- a/src/client/elm/Model/InitResult.elm +++ b/src/client/elm/Model/InitResult.elm @@ -3,7 +3,7 @@ module Model.InitResult exposing , initResultDecoder ) -import Json.Decode as Json exposing ((:=)) +import Json.Decode as Decode exposing (Decoder) import Model.Init exposing (Init, initDecoder) @@ -12,17 +12,17 @@ type InitResult = | InitSuccess Init | InitError String -initResultDecoder : Json.Decoder InitResult -initResultDecoder = ("tag" := Json.string) `Json.andThen` initResultDecoderWithTag +initResultDecoder : Decoder InitResult +initResultDecoder = (Decode.field "tag" Decode.string) |> Decode.andThen initResultDecoderWithTag -initResultDecoderWithTag : String -> Json.Decoder InitResult +initResultDecoderWithTag : String -> Decoder InitResult initResultDecoderWithTag tag = case tag of "InitEmpty" -> - Json.succeed InitEmpty + Decode.succeed InitEmpty "InitSuccess" -> - Json.map InitSuccess ("contents" := initDecoder) + Decode.map InitSuccess (Decode.field "contents" initDecoder) "InitError" -> - Json.map InitError ("contents" := Json.string) + Decode.map InitError (Decode.field "contents" Decode.string) _ -> - Json.fail <| "got " ++ tag ++ " for InitResult" + Decode.fail <| "got " ++ tag ++ " for InitResult" diff --git a/src/client/elm/Model/Payer.elm b/src/client/elm/Model/Payer.elm index e5a4b65..1663273 100644 --- a/src/client/elm/Model/Payer.elm +++ b/src/client/elm/Model/Payer.elm @@ -6,7 +6,6 @@ module Model.Payer exposing , useIncomesFrom ) -import Json.Decode as Json exposing (..) import Dict exposing (..) import List import Maybe @@ -54,7 +53,7 @@ getOrderedExceedingPayers currentTime users incomes payments = mbMaxRatio = postPaymentPayers |> Dict.toList - |> List.map (.ratio << snd) + |> List.map (.ratio << Tuple.second) |> List.maximum in case mbMaxRatio of Just maxRatio -> @@ -110,15 +109,15 @@ getPayers currentTime users incomes payments = exceedingPayersFromAmounts : List (UserId, Int) -> List ExceedingPayer exceedingPayersFromAmounts userAmounts = - let mbMinAmount = List.minimum << List.map snd <| userAmounts + let mbMinAmount = List.minimum << List.map Tuple.second <| userAmounts in case mbMinAmount of Nothing -> [] Just minAmount -> userAmounts |> List.map (\userAmount -> - { userId = fst userAmount - , amount = snd userAmount - minAmount + { userId = Tuple.first userAmount + , amount = Tuple.second userAmount - minAmount } ) |> List.filter (\payer -> payer.amount > 0) diff --git a/src/client/elm/Model/Payment.elm b/src/client/elm/Model/Payment.elm index 5109b2f..f61ded8 100644 --- a/src/client/elm/Model/Payment.elm +++ b/src/client/elm/Model/Payment.elm @@ -6,6 +6,7 @@ module Model.Payment exposing , Frequency(..) , paymentsDecoder , paymentIdDecoder + , find , edit , delete , totalPayments @@ -18,15 +19,16 @@ module Model.Payment exposing import Date exposing (..) import Date.Extra.Core exposing (monthToInt, intToMonth) -import Json.Decode as Json exposing ((:=)) -import String +import Json.Decode as Decode exposing (Decoder) +import Json.Decode.Extra as Decode import List import Form.Validate as Validate exposing (Validation) -import Model.User exposing (UserId, userIdDecoder) import Model.Date exposing (dateDecoder) +import Model.User exposing (UserId, userIdDecoder) import Utils.List as List +import Utils.Search as Search perPage : Int perPage = 7 @@ -46,31 +48,36 @@ type alias PaymentId = Int type Frequency = Punctual | Monthly -paymentsDecoder : Json.Decoder Payments -paymentsDecoder = Json.list paymentDecoder +paymentsDecoder : Decoder Payments +paymentsDecoder = Decode.list paymentDecoder -paymentDecoder : Json.Decoder Payment +paymentDecoder : Decoder Payment paymentDecoder = - Json.object6 Payment - ("id" := paymentIdDecoder) - ("name" := Json.string) - ("cost" := Json.int) - ("date" := dateDecoder) - ("userId" := userIdDecoder) - ("frequency" := frequencyDecoder) - -paymentIdDecoder : Json.Decoder PaymentId -paymentIdDecoder = Json.int - -frequencyDecoder : Json.Decoder Frequency + Decode.map6 Payment + (Decode.field "id" paymentIdDecoder) + (Decode.field "name" Decode.string) + (Decode.field "cost" Decode.int) + (Decode.field "date" dateDecoder) + (Decode.field "userId" userIdDecoder) + (Decode.field "frequency" frequencyDecoder) + +paymentIdDecoder : Decoder PaymentId +paymentIdDecoder = Decode.int + +frequencyDecoder : Decoder Frequency frequencyDecoder = - Json.customDecoder - Json.string - (\input -> case input of - "Punctual" -> Ok Punctual - "Monthly" -> Ok Monthly - _ -> Err ("Could not deduce Punctual nor Monthly from " ++ input) - ) + let frequencyResult input = + case input of + "Punctual" -> Ok Punctual + "Monthly" -> Ok Monthly + _ -> Err ("Could not deduce Punctual nor Monthly from " ++ input) + in Decode.string |> Decode.andThen (Decode.fromResult << frequencyResult) + +find : PaymentId -> Payments -> Maybe Payment +find paymentId payments = + payments + |> List.filter (\p -> p.id == paymentId) + |> List.head edit : Payment -> Payments -> Payments edit payment payments = payment :: delete payment.id payments @@ -98,7 +105,7 @@ groupAndSortByMonth : Payments -> List ((Month, Int), Payments) groupAndSortByMonth payments = payments |> List.groupBy (\payment -> (Date.year payment.date, monthToInt << Date.month <| payment.date)) - |> List.sortBy fst + |> List.sortBy Tuple.first |> List.map (\((year, month), payments) -> ((intToMonth month, year), payments)) |> List.reverse @@ -118,7 +125,7 @@ paymentSort frequency = searchSuccess : String -> Payment -> Bool searchSuccess search { name, cost } = let searchSuccessWord word = - ( String.contains (String.toLower word) (String.toLower name) + ( String.contains (Search.format word) (Search.format name) || String.contains word (toString cost) ) in List.all searchSuccessWord (String.words search) diff --git a/src/client/elm/Model/PaymentCategory.elm b/src/client/elm/Model/PaymentCategory.elm new file mode 100644 index 0000000..87678fe --- /dev/null +++ b/src/client/elm/Model/PaymentCategory.elm @@ -0,0 +1,48 @@ +module Model.PaymentCategory exposing + ( PaymentCategories + , paymentCategoriesDecoder + , search + , isCategoryUnused + , set + , update + ) + +import Dict exposing (Dict) +import Json.Decode as Decode exposing (Decoder) + +import Model.Category exposing (CategoryId, categoryIdDecoder) +import Utils.Json as Json +import Utils.Search as Search + +type alias PaymentCategories = List PaymentCategory + +type alias PaymentCategory = + { name : String + , category : CategoryId + } + +paymentCategoriesDecoder : Decoder PaymentCategories +paymentCategoriesDecoder = + Decode.list <| Decode.map2 PaymentCategory + (Decode.field "name" Decode.string) + (Decode.field "category" categoryIdDecoder) + +search : String -> PaymentCategories -> Maybe CategoryId +search paymentName paymentCategories = + paymentCategories + |> List.filter (\pc -> Search.format pc.name == Search.format paymentName) + |> List.head + |> Maybe.map .category + +isCategoryUnused : CategoryId -> PaymentCategories -> Bool +isCategoryUnused category paymentCategories = + paymentCategories + |> List.filter ((==) category << .category) + |> List.isEmpty + +set : String -> CategoryId -> PaymentCategories -> PaymentCategories +set name category paymentCategories = update name name category paymentCategories + +update : String -> String -> CategoryId -> PaymentCategories -> PaymentCategories +update oldName newName category paymentCategories = + { name = newName, category = category } :: List.filter (\pc -> not <| Search.format pc.name == Search.format oldName) paymentCategories diff --git a/src/client/elm/Model/Size.elm b/src/client/elm/Model/Size.elm index b29e90b..f40fb01 100644 --- a/src/client/elm/Model/Size.elm +++ b/src/client/elm/Model/Size.elm @@ -3,15 +3,15 @@ module Model.Size exposing , sizeDecoder ) -import Json.Decode as Json exposing ((:=)) +import Json.Decode as Decode exposing (Decoder) type alias Size = { width: Int , height: Int } -sizeDecoder : Json.Decoder Size +sizeDecoder : Decoder Size sizeDecoder = - Json.object2 Size - ("width" := Json.int) - ("height" := Json.int) + Decode.map2 Size + (Decode.field "width" Decode.int) + (Decode.field "height" Decode.int) diff --git a/src/client/elm/Model/Translations.elm b/src/client/elm/Model/Translations.elm index 57409b0..9b314e1 100644 --- a/src/client/elm/Model/Translations.elm +++ b/src/client/elm/Model/Translations.elm @@ -7,13 +7,13 @@ module Model.Translations exposing ) import Maybe exposing (withDefault) -import Json.Decode as Json exposing ((:=)) +import Json.Decode as Decode exposing (Decoder) import String type alias Translations = List Translation -translationsDecoder : Json.Decoder Translations -translationsDecoder = Json.list translationDecoder +translationsDecoder : Decoder Translations +translationsDecoder = Decode.list translationDecoder type alias Translation = { key : String @@ -27,25 +27,24 @@ getTranslation key translations = |> List.head |> Maybe.map .message -translationDecoder : Json.Decoder Translation +translationDecoder : Decoder Translation translationDecoder = - Json.object2 Translation - ("key" := Json.string) - ("message" := Json.list partDecoder) + Decode.map2 Translation + (Decode.field "key" Decode.string) + (Decode.field "message" (Decode.list partDecoder)) type MessagePart = Order Int | Str String -partDecoder : Json.Decoder MessagePart -partDecoder = - ("tag" := Json.string) `Json.andThen` partDecoderWithTag +partDecoder : Decoder MessagePart +partDecoder = (Decode.field "tag" Decode.string) |> Decode.andThen partDecoderWithTag -partDecoderWithTag : String -> Json.Decoder MessagePart +partDecoderWithTag : String -> Decoder MessagePart partDecoderWithTag tag = case tag of - "Order" -> Json.object1 Order ("contents" := Json.int) - _ -> Json.object1 Str ("contents" := Json.string) + "Order" -> Decode.map Order (Decode.field "contents" Decode.int) + _ -> Decode.map Str (Decode.field "contents" Decode.string) ----- diff --git a/src/client/elm/Model/User.elm b/src/client/elm/Model/User.elm index 02f2cea..f6e8147 100644 --- a/src/client/elm/Model/User.elm +++ b/src/client/elm/Model/User.elm @@ -8,7 +8,7 @@ module Model.User exposing , getUserName ) -import Json.Decode as Json exposing ((:=)) +import Json.Decode as Decode exposing (Decoder) import Dict exposing (Dict) type alias Users = Dict UserId User @@ -20,23 +20,23 @@ type alias User = , email : String } -usersDecoder : Json.Decoder Users -usersDecoder = Json.map Dict.fromList (Json.list userWithIdDecoder) +usersDecoder : Decoder Users +usersDecoder = Decode.map Dict.fromList (Decode.list userWithIdDecoder) -userWithIdDecoder : Json.Decoder (UserId, User) +userWithIdDecoder : Decode.Decoder (UserId, User) userWithIdDecoder = - Json.object2 (,) - ("id" := userIdDecoder) + Decode.map2 (,) + (Decode.field "id" userIdDecoder) userDecoder -userIdDecoder : Json.Decoder UserId -userIdDecoder = Json.int +userIdDecoder : Decoder UserId +userIdDecoder = Decode.int -userDecoder : Json.Decoder User +userDecoder : Decoder User userDecoder = - Json.object2 User - ("name" := Json.string) - ("email" := Json.string) + Decode.map2 User + (Decode.field "name" Decode.string) + (Decode.field "email" Decode.string) getUserName : Users -> UserId -> Maybe String getUserName users userId = diff --git a/src/client/elm/Msg.elm b/src/client/elm/Msg.elm index 49d13ca..cf592aa 100644 --- a/src/client/elm/Msg.elm +++ b/src/client/elm/Msg.elm @@ -10,6 +10,7 @@ import Page exposing (Page) import Model.Init exposing (Init) import Model.Payment exposing (PaymentId, Frequency) import Model.Income exposing (IncomeId) +import Model.Category exposing (CategoryId) import Dialog import Dialog.Model as DialogModel @@ -22,19 +23,26 @@ import LoggedIn.Msg as LoggedInMsg type Msg = NoOp + | UpdatePage Page | SignIn String | UpdateTime Time | GoLoggedInView Init | UpdateSignIn SignInMsg.Msg | UpdateLoggedIn LoggedInMsg.Msg - | CreatePayment String Int Date Frequency - | EditPayment PaymentId String Int Date Frequency - | DeletePayment PaymentId - | CreateIncome Int Date - | EditIncome IncomeId Int Date - | DeleteIncome IncomeId | GoSignInView | SignOut | Error String | Dialog (Dialog.Msg DialogModel.Model DialogMsg.Msg Msg) | Tooltip Tooltip.Msg + + | CreatePayment String Int Date CategoryId Frequency + | EditPayment PaymentId String Int Date CategoryId Frequency + | DeletePayment PaymentId + + | CreateIncome Int Date + | EditIncome IncomeId Int Date + | DeleteIncome IncomeId + + | CreateCategory String String + | EditCategory CategoryId String String + | DeleteCategory CategoryId diff --git a/src/client/elm/Page.elm b/src/client/elm/Page.elm index 7cfbbc7..39232e0 100644 --- a/src/client/elm/Page.elm +++ b/src/client/elm/Page.elm @@ -1,32 +1,43 @@ module Page exposing ( Page(..) , toHash - , fromHash + , fromLocation ) -import Navigation -import UrlParser exposing (..) +import Navigation exposing (Location) +import UrlParser exposing (Parser, (), s) import String type Page = Home | Income + | Categories | Statistics + | NotFound toHash : Page -> String toHash page = case page of Home -> "#" Income -> "#income" + Categories -> "#categories" Statistics -> "#statistics" + NotFound -> "#notFound" -fromHash : Navigation.Location -> Result String Page -fromHash location = UrlParser.parse identity pageParser (String.dropLeft 1 location.hash) +fromLocation : Location -> Page +fromLocation location = + if location.hash == "" + then + Home + else + case UrlParser.parseHash pageParser location of + Just page -> page + Nothing -> NotFound pageParser : Parser (Page -> a) a pageParser = - oneOf - [ format Home (s "") - , format Income (s "income") - , format Statistics (s "statistics") + UrlParser.oneOf + [ UrlParser.map Income (s "income") + , UrlParser.map Categories (s "categories") + , UrlParser.map Statistics (s "statistics") ] diff --git a/src/client/elm/Server.elm b/src/client/elm/Server.elm index c017548..7f25876 100644 --- a/src/client/elm/Server.elm +++ b/src/client/elm/Server.elm @@ -6,14 +6,17 @@ module Server exposing , createIncome , editIncome , deleteIncome + , createCategory + , editCategory + , deleteCategory , signOut ) import Task as Task exposing (Task) -import Http +import Http exposing (Error) import Date -import Json.Decode exposing ((:=)) -import Json.Encode as Json +import Json.Decode as Decode +import Json.Encode as Encode import Date exposing (Date) import Date.Extra.Format as DateFormat @@ -21,68 +24,91 @@ import Date.Extra.Format as DateFormat import Utils.Http as HttpUtils import Model.Payment exposing (..) -import Model.Income exposing (incomesDecoder, incomeIdDecoder, IncomeId) +import Model.Income exposing (incomeIdDecoder, IncomeId) +import Model.Category exposing (categoryIdDecoder, CategoryId) import Model.User exposing (Users, usersDecoder, UserId, userIdDecoder) import Model.Init exposing (Init) -signIn : String -> Task Http.Error () -signIn email = - HttpUtils.request "POST" ("/signIn?email=" ++ email) - |> Task.map (always ()) +signIn : String -> (Result Error String -> msg) -> Cmd msg +signIn email = HttpUtils.request "POST" ("/signIn?email=" ++ email) Http.expectString -createPayment : String -> Int -> Date -> Frequency -> Task Http.Error PaymentId -createPayment name cost date frequency = - Json.object - [ ("name", Json.string name) - , ("cost", Json.int cost) - , ("date", Json.string (DateFormat.isoDateString date)) - , ("frequency", Json.string (toString frequency)) - ] - |> HttpUtils.jsonRequest "POST" "/payment" - |> flip Task.andThen (HttpUtils.decodeHttpValue <| "id" := paymentIdDecoder) +createPayment : String -> Int -> Date -> CategoryId -> Frequency -> (Result Error PaymentId -> msg) -> Cmd msg +createPayment name cost date categoryId frequency handleResult = + let json = + Encode.object + [ ("name", Encode.string name) + , ("cost", Encode.int cost) + , ("date", Encode.string (DateFormat.isoDateString date)) + , ("category", Encode.int categoryId) + , ("frequency", Encode.string (toString frequency)) + ] + expect = Http.expectJson (Decode.field "id" paymentIdDecoder) + in HttpUtils.jsonRequest "POST" "/payment" expect handleResult json -editPayment : PaymentId -> String -> Int -> Date -> Frequency -> Task Http.Error () -editPayment paymentId name cost date frequency = - Json.object - [ ("id", Json.int paymentId) - , ("name", Json.string name) - , ("cost", Json.int cost) - , ("date", Json.string (DateFormat.isoDateString date)) - , ("frequency", Json.string (toString frequency)) - ] - |> HttpUtils.jsonRequest "PUT" "/payment" - |> Task.map (always ()) +editPayment : PaymentId -> String -> Int -> Date -> CategoryId -> Frequency -> (Result Error String -> msg) -> Cmd msg +editPayment paymentId name cost date categoryId frequency handleResult = + let json = + Encode.object + [ ("id", Encode.int paymentId) + , ("name", Encode.string name) + , ("cost", Encode.int cost) + , ("date", Encode.string (DateFormat.isoDateString date)) + , ("category", Encode.int categoryId) + , ("frequency", Encode.string (toString frequency)) + ] + in HttpUtils.jsonRequest "PUT" "/payment" Http.expectString handleResult json -deletePayment : PaymentId -> Task Http.Error () +deletePayment : PaymentId -> (Result Error String -> msg) -> Cmd msg deletePayment paymentId = - HttpUtils.request "DELETE" ("/payment?id=" ++ (toString paymentId)) - |> Task.map (always ()) + HttpUtils.request "DELETE" ("/payment?id=" ++ (toString paymentId)) Http.expectString -createIncome : Int -> Date -> Task Http.Error IncomeId -createIncome amount date = - Json.object - [ ("amount", Json.int amount) - , ("date", Json.string (DateFormat.isoDateString date)) - ] - |> HttpUtils.jsonRequest "POST" "/income" - |> flip Task.andThen (HttpUtils.decodeHttpValue <| "id" := incomeIdDecoder) +createIncome : Int -> Date -> (Result Error IncomeId -> msg) -> Cmd msg +createIncome amount date handleResult = + let json = + Encode.object + [ ("amount", Encode.int amount) + , ("date", Encode.string (DateFormat.isoDateString date)) + ] + expect = Http.expectJson (Decode.field "id" incomeIdDecoder) + in HttpUtils.jsonRequest "POST" "/income" expect handleResult json -editIncome : IncomeId -> Int -> Date -> Task Http.Error () -editIncome incomeId amount date = - Json.object - [ ("id", Json.int incomeId) - , ("amount", Json.int amount) - , ("date", Json.string (DateFormat.isoDateString date)) - ] - |> HttpUtils.jsonRequest "PUT" "/income" - |> Task.map (always ()) +editIncome : IncomeId -> Int -> Date -> (Result Error String -> msg) -> Cmd msg +editIncome incomeId amount date handleResult = + let json = + Encode.object + [ ("id", Encode.int incomeId) + , ("amount", Encode.int amount) + , ("date", Encode.string (DateFormat.isoDateString date)) + ] + in HttpUtils.jsonRequest "PUT" "/income" Http.expectString handleResult json -deleteIncome : IncomeId -> Task Http.Error () +deleteIncome : IncomeId -> (Result Error String -> msg) -> Cmd msg deleteIncome incomeId = - HttpUtils.request "DELETE" ("/income?id=" ++ (toString incomeId)) - |> Task.map (always ()) + HttpUtils.request "DELETE" ("/income?id=" ++ (toString incomeId)) Http.expectString -signOut : Task Http.Error () -signOut = - HttpUtils.request "POST" "/signOut" - |> Task.map (always ()) +createCategory : String -> String -> (Result Error CategoryId -> msg) -> Cmd msg +createCategory name color handleResult = + let json = + Encode.object + [ ("name", Encode.string name) + , ("color", Encode.string color) + ] + expect = Http.expectJson (Decode.field "id" categoryIdDecoder) + in HttpUtils.jsonRequest "POST" "/category" expect handleResult json + +editCategory : CategoryId -> String -> String -> (Result Error String -> msg) -> Cmd msg +editCategory categoryId name color handleResult = + let json = + Encode.object + [ ("id", Encode.int categoryId) + , ("name", Encode.string name) + , ("color", Encode.string color) + ] + in HttpUtils.jsonRequest "PUT" "/category" Http.expectString handleResult json + +deleteCategory : CategoryId -> (Result Error String -> msg) -> Cmd msg +deleteCategory categoryId = + HttpUtils.request "DELETE" ("/category?id=" ++ (toString categoryId)) Http.expectString + +signOut : (Result Error String -> msg) -> Cmd msg +signOut = HttpUtils.request "POST" "/signOut" Http.expectString diff --git a/src/client/elm/SignIn/View.elm b/src/client/elm/SignIn/View.elm index f23ca09..88f74b0 100644 --- a/src/client/elm/SignIn/View.elm +++ b/src/client/elm/SignIn/View.elm @@ -2,7 +2,7 @@ module SignIn.View exposing ( view ) -import Json.Decode as Json +import Json.Decode as Decode import FontAwesome import View.Color as Color @@ -30,7 +30,7 @@ view model signInModel = [ onSubmitPrevDefault (SignIn signInModel.login) ] [ input [ value signInModel.login - , on "input" (targetValue |> (Json.map <| (UpdateSignIn << SignInMsg.UpdateLogin))) + , on "input" (targetValue |> (Decode.map <| (UpdateSignIn << SignInMsg.UpdateLogin))) , name "email" ] [] diff --git a/src/client/elm/Update.elm b/src/client/elm/Update.elm index e66414e..7006d5a 100644 --- a/src/client/elm/Update.elm +++ b/src/client/elm/Update.elm @@ -1,11 +1,10 @@ module Update exposing ( update - , urlUpdate ) import Task import Platform.Cmd exposing (Cmd) -import Navigation +import Navigation exposing (Location) import Page exposing (Page) @@ -32,7 +31,6 @@ import Tooltip import Utils.Http exposing (errorKey) import Utils.Cmd exposing ((:>)) -import Utils.Tuple as Tuple update : Msg -> Model -> (Model, Cmd Msg) update msg model = @@ -41,12 +39,15 @@ update msg model = NoOp -> (model, Cmd.none) + UpdatePage page -> + ({ model | page = page }, Cmd.none) + SignIn email -> ( applySignIn model (SignInMsg.WaitingServer) - , Server.signIn email - |> Task.perform - (\error -> UpdateSignIn (SignInMsg.ErrorLogin (errorKey error))) - (\() -> UpdateSignIn SignInMsg.ValidLogin) + , Server.signIn email (\result -> case result of + Ok _ -> UpdateSignIn SignInMsg.ValidLogin + Err error -> UpdateSignIn (SignInMsg.ErrorLogin (errorKey error)) + ) ) GoLoggedInView init -> @@ -66,73 +67,100 @@ update msg model = UpdateLoggedIn loggedInMsg -> applyLoggedIn model loggedInMsg - CreatePayment name cost date frequency -> + SignOut -> + ( model + , Server.signOut (\result -> case result of + Ok _ -> GoSignInView + Err _ -> Error "SignOutError" + ) + ) + + Error error -> + ({ model | errors = model.errors ++ [ error ] }, Cmd.none) + + Dialog dialogMsg -> + Dialog.update DialogUpdate.update dialogMsg model.dialog.model model.dialog + |> Tuple.mapFirst (\dialog -> { model | dialog = dialog }) + :> update (Tooltip Tooltip.HideMessage) + + Tooltip tooltipMsg -> + let (newTooltip, command) = Tooltip.update tooltipMsg model.tooltip + in ( { model | tooltip = newTooltip } + , Cmd.map Tooltip command + ) + + CreatePayment name cost date category frequency -> ( model - , Server.createPayment name cost date frequency - |> Task.perform - (always <| Error "CreatePaymentError") - (\paymentId -> UpdateLoggedIn <| LoggedInMsg.ValidateCreatePayment paymentId name cost date frequency) + , Server.createPayment name cost date category frequency (\result -> case result of + Ok paymentId -> UpdateLoggedIn <| LoggedInMsg.ValidateCreatePayment paymentId name cost date category frequency + Err _ -> Error "CreatePaymentError" + ) ) - EditPayment paymentId name cost date frequency -> + EditPayment paymentId name cost date category frequency -> ( model - , Server.editPayment paymentId name cost date frequency - |> Task.perform - (always <| Error "EditPaymentError") - (always <| UpdateLoggedIn <| LoggedInMsg.ValidateEditPayment paymentId name cost date frequency) + , Server.editPayment paymentId name cost date category frequency (\result -> case result of + Ok _ -> UpdateLoggedIn <| LoggedInMsg.ValidateEditPayment paymentId name cost date category frequency + Err _ -> Error "EditPaymentError" + ) ) DeletePayment paymentId -> ( model - , Server.deletePayment paymentId - |> Task.perform - (always <| Error "DeletePaymentError") - (always <| UpdateLoggedIn <| LoggedInMsg.ValidateDeletePayment paymentId) + , Server.deletePayment paymentId (\result -> case result of + Ok _ -> UpdateLoggedIn <| LoggedInMsg.ValidateDeletePayment paymentId + Err _ -> Error "DeletePaymentError" + ) ) CreateIncome amount date -> ( model - , Server.createIncome amount date - |> Task.perform - (always <| Error "CreateIncomeError") - (\incomeId -> UpdateLoggedIn <| LoggedInMsg.ValidateCreateIncome incomeId amount date) + , Server.createIncome amount date (\result -> case result of + Ok incomeId -> UpdateLoggedIn <| LoggedInMsg.ValidateCreateIncome incomeId amount date + Err _ -> Error "CreateIncomeError" + ) ) EditIncome incomeId amount date -> ( model - , Server.editIncome incomeId amount date - |> Task.perform - (always <| Error "EditIncomeError") - (always <| UpdateLoggedIn <| LoggedInMsg.ValidateEditIncome incomeId amount date) + , Server.editIncome incomeId amount date (\result -> case result of + Ok _ -> UpdateLoggedIn <| LoggedInMsg.ValidateEditIncome incomeId amount date + Err _ -> Error "EditIncomeError" + ) ) DeleteIncome incomeId -> ( model - , Server.deleteIncome incomeId - |> Task.perform - (always <| Error "DeleteIncomeError") - (always <| UpdateLoggedIn <| LoggedInMsg.ValidateDeleteIncome incomeId) + , Server.deleteIncome incomeId (\result -> case result of + Ok _ -> UpdateLoggedIn <| LoggedInMsg.ValidateDeleteIncome incomeId + Err _ -> Error "DeleteIncomeError" + ) ) - SignOut -> + CreateCategory name color -> ( model - , Server.signOut - |> Task.perform (always <| Error "SignOutError") (always GoSignInView) + , Server.createCategory name color (\result -> case result of + Ok categoryId -> UpdateLoggedIn <| LoggedInMsg.ValidateCreateCategory categoryId name color + Err _ -> Error "CreateCategoryError" + ) ) - Error error -> - ({ model | errors = model.errors ++ [ error ] }, Cmd.none) + EditCategory categoryId name color -> + ( model + , Server.editCategory categoryId name color (\result -> case result of + Ok _ -> UpdateLoggedIn <| LoggedInMsg.ValidateEditCategory categoryId name color + Err _ -> Error "EditCategoryError" + ) + ) - Dialog dialogMsg -> - Dialog.update DialogUpdate.update dialogMsg model.dialog.model model.dialog - |> Tuple.mapFst (\dialog -> { model | dialog = dialog }) - :> update (Tooltip Tooltip.HideMessage) + DeleteCategory categoryId -> + ( model + , Server.deleteCategory categoryId (\result -> case result of + Ok _ -> UpdateLoggedIn <| LoggedInMsg.ValidateDeleteCategory categoryId + Err _ -> Error "DeleteCategoryError" + ) + ) - Tooltip tooltipMsg -> - let (newTooltip, command) = Tooltip.update tooltipMsg model.tooltip - in ( { model | tooltip = newTooltip } - , Cmd.map Tooltip command - ) applySignIn : Model -> SignInMsg.Msg -> Model applySignIn model signInMsg = @@ -146,17 +174,9 @@ applyLoggedIn : Model -> LoggedInMsg.Msg -> (Model, Cmd Msg) applyLoggedIn model loggedInMsg = case model.view of V.LoggedInView loggedInView -> - let (loggedInView, cmd) = LoggedInUpdate.update model loggedInMsg loggedInView - in ( { model | view = V.LoggedInView loggedInView } + let (view, cmd) = LoggedInUpdate.update model loggedInMsg loggedInView + in ( { model | view = V.LoggedInView view } , Cmd.map UpdateLoggedIn cmd ) _ -> (model, Cmd.none) - -urlUpdate : Result String Page -> Model -> (Model, Cmd Msg) -urlUpdate result model = - case result of - Err _ -> - (model, Navigation.modifyUrl (Page.toHash model.page)) - Ok page -> - ({ model | page = page }, Cmd.none) diff --git a/src/client/elm/Utils/Cmd.elm b/src/client/elm/Utils/Cmd.elm index 8b79446..5f41cbe 100644 --- a/src/client/elm/Utils/Cmd.elm +++ b/src/client/elm/Utils/Cmd.elm @@ -7,8 +7,8 @@ import Platform.Cmd as Cmd pipeUpdate : (model, Cmd msg) -> (model -> (model, Cmd msg)) -> (model, Cmd msg) pipeUpdate (model, cmd) f = - let (model', cmd') = f model - in (model', Cmd.batch [ cmd, cmd' ]) + let (newModel, newCmd) = f model + in (newModel, Cmd.batch [ cmd, newCmd ]) (:>) : (m, Cmd a) -> (m -> (m, Cmd a)) -> (m, Cmd a) (:>) = pipeUpdate diff --git a/src/client/elm/Utils/Http.elm b/src/client/elm/Utils/Http.elm index 4edc233..dd3870a 100644 --- a/src/client/elm/Utils/Http.elm +++ b/src/client/elm/Utils/Http.elm @@ -1,69 +1,39 @@ module Utils.Http exposing ( jsonRequest , request - , requestWithBody - , decodeHttpValue , errorKey ) import Http exposing (..) import Task exposing (..) -import Json.Decode as JsonDecode exposing (Decoder) -import Json.Encode as JsonEncode - -jsonRequest : String -> String -> JsonEncode.Value -> Task Error Value -jsonRequest method url json = - json - |> JsonEncode.encode 0 - |> Http.string - |> requestWithBody method url - -request : String -> String -> Task Error Value -request method url = requestWithBody method url empty - -requestWithBody : String -> String -> Body -> Task Error Value -requestWithBody method url body = - { verb = method - , headers = [] - , url = url - , body = body - } - |> Http.send defaultSettings - |> mapError promoteError - |> flip Task.andThen handleResponse - -promoteError : RawError -> Error -promoteError rawError = - case rawError of - RawTimeout -> Timeout - RawNetworkError -> NetworkError - -handleResponse : Response -> Task Error Value -handleResponse response = - if 200 <= response.status && response.status < 300 - then Task.succeed response.value - else fail (BadResponse response.status (responseString response.value)) - -responseString : Value -> String -responseString value = - case value of - Text str -> str - _ -> "" - -decodeHttpValue : Decoder a -> Value -> Task Error a -decodeHttpValue decoder value = - case value of - Text str -> - case JsonDecode.decodeString decoder str of - Ok v -> succeed v - Err msg -> fail (UnexpectedPayload msg) - _ -> - fail (UnexpectedPayload "Response body is a blob, expecting a string.") +import Json.Decode as Decode exposing (Decoder, Value) +import Json.Encode as Encode + +jsonRequest : String -> String -> Expect a -> (Result Error a -> msg) -> Encode.Value -> Cmd msg +jsonRequest method url expect handleResult value = + requestWithBody method url (jsonBody value) expect handleResult + +request : String -> String -> Expect a -> (Result Error a -> msg) -> Cmd msg +request method url = requestWithBody method url emptyBody + +requestWithBody : String -> String -> Body -> Expect a -> (Result Error a -> msg) -> Cmd msg +requestWithBody method url body expect handleResult = + let req = Http.request + { method = method + , headers = [] + , url = url + , body = body + , expect = expect + , timeout = Nothing + , withCredentials = False + } + in send handleResult req errorKey : Error -> String errorKey error = case error of + BadUrl _ -> "BadUrl" Timeout -> "Timeout" NetworkError -> "NetworkError" - UnexpectedPayload _ -> "UnexpectedPayload" - BadResponse _ key -> key + BadPayload _ _ -> "BadPayload" + BadStatus response -> response.body diff --git a/src/client/elm/Utils/Json.elm b/src/client/elm/Utils/Json.elm new file mode 100644 index 0000000..29e815b --- /dev/null +++ b/src/client/elm/Utils/Json.elm @@ -0,0 +1,12 @@ +module Utils.Json exposing + ( dictDecoder + ) + +import Json.Decode as Decode exposing (Decoder) +import Dict exposing (Dict) + +dictDecoder : Decoder comparable -> Decoder a -> Decoder (Dict comparable a) +dictDecoder keyDecoder valueDecoder = + Decode.map2 (,) keyDecoder valueDecoder + |> Decode.list + |> Decode.map Dict.fromList diff --git a/src/client/elm/Utils/Maybe.elm b/src/client/elm/Utils/Maybe.elm index 4a94aa5..46456e1 100644 --- a/src/client/elm/Utils/Maybe.elm +++ b/src/client/elm/Utils/Maybe.elm @@ -1,7 +1,8 @@ module Utils.Maybe exposing ( isJust - , catMaybes - , maybeToList + , cat + , toList + , orElse ) isJust : Maybe a -> Bool @@ -10,8 +11,8 @@ isJust maybe = Just _ -> True Nothing -> False -catMaybes : List (Maybe a) -> List a -catMaybes = +cat : List (Maybe a) -> List a +cat = List.foldr (\mb xs -> case mb of @@ -20,8 +21,14 @@ catMaybes = ) [] -maybeToList : Maybe a -> List a -maybeToList mb = +toList : Maybe a -> List a +toList mb = case mb of Just a -> [a] Nothing -> [] + +orElse : Maybe a -> Maybe a -> Maybe a +orElse mb1 mb2 = + case mb1 of + Just x -> Just x + Nothing -> mb2 diff --git a/src/client/elm/Utils/Search.elm b/src/client/elm/Utils/Search.elm new file mode 100644 index 0000000..1b70387 --- /dev/null +++ b/src/client/elm/Utils/Search.elm @@ -0,0 +1,10 @@ +module Utils.Search exposing + ( format + ) + +import String + +import Utils.String as String + +format : String -> String +format = String.unaccent << String.toLower diff --git a/src/client/elm/Utils/String.elm b/src/client/elm/Utils/String.elm new file mode 100644 index 0000000..90fe68e --- /dev/null +++ b/src/client/elm/Utils/String.elm @@ -0,0 +1,38 @@ +module Utils.String exposing + ( unaccent + ) + +unaccent : String -> String +unaccent = String.map unaccentChar + +unaccentChar : Char -> Char +unaccentChar c = case c of + 'à' -> 'a' + 'á' -> 'a' + 'â' -> 'a' + 'ã' -> 'a' + 'ä' -> 'a' + 'ç' -> 'c' + 'è' -> 'e' + 'é' -> 'e' + 'ê' -> 'e' + 'ë' -> 'e' + 'ì' -> 'i' + 'í' -> 'i' + 'î' -> 'i' + 'ï' -> 'i' + 'ñ' -> 'n' + 'ò' -> 'o' + 'ó' -> 'o' + 'ô' -> 'o' + 'õ' -> 'o' + 'ö' -> 'o' + 'š' -> 's' + 'ù' -> 'u' + 'ú' -> 'u' + 'û' -> 'u' + 'ü' -> 'u' + 'ý' -> 'y' + 'ÿ' -> 'y' + 'ž' -> 'z' + _ -> c diff --git a/src/client/elm/Utils/Tuple.elm b/src/client/elm/Utils/Tuple.elm deleted file mode 100644 index f9391a0..0000000 --- a/src/client/elm/Utils/Tuple.elm +++ /dev/null @@ -1,14 +0,0 @@ -module Utils.Tuple exposing - ( mapFst - , mapSnd - , mapBoth - ) - -mapFst : (a -> x) -> (a, b) -> (x, b) -mapFst f (a, b) = (f a, b) - -mapSnd : (b -> x) -> (a, b) -> (a, x) -mapSnd f (a, b) = (a, f b) - -mapBoth : (a -> x) -> (b -> y) -> (a, b) -> (x, y) -mapBoth f g (a, b) = (f a, g b) diff --git a/src/client/elm/Validation.elm b/src/client/elm/Validation.elm index 1729daa..18b3934 100644 --- a/src/client/elm/Validation.elm +++ b/src/client/elm/Validation.elm @@ -1,14 +1,18 @@ module Validation exposing ( date + , category ) -import String exposing (toInt, split) import Date exposing (Date) -import Date.Extra.Create exposing (dateFromFields) import Date.Extra.Core exposing (intToMonth) +import Date.Extra.Create exposing (dateFromFields) +import Dict +import String exposing (toInt, split) import Form.Validate as Validate exposing (..) +import Model.Category exposing (Categories, CategoryId) + date : Validation String Date date = customValidation string (\str -> @@ -20,3 +24,15 @@ date = _ -> Err (customError "InvalidDate") _ -> Err (customError "InvalidDate") ) + +category : Categories -> Validation String CategoryId +category categories = + customValidation string (\str -> + case toInt str of + Ok category -> + if List.member category (Dict.keys categories) + then Ok category + else Err (customError "InvalidCategory") + Err _ -> + Err (customError "InvalidCategory") + ) diff --git a/src/client/elm/View.elm b/src/client/elm/View.elm index 4a0822f..66c498a 100644 --- a/src/client/elm/View.elm +++ b/src/client/elm/View.elm @@ -3,7 +3,6 @@ module View exposing ) import Html exposing (..) -import Html.App as Html import Html.Attributes exposing (..) import Model exposing (Model) diff --git a/src/client/elm/View/Events.elm b/src/client/elm/View/Events.elm index c50fe98..d71d67d 100644 --- a/src/client/elm/View/Events.elm +++ b/src/client/elm/View/Events.elm @@ -2,7 +2,7 @@ module View.Events exposing ( onSubmitPrevDefault ) -import Json.Decode as Json +import Json.Decode as Decode import Html exposing (..) import Html.Events exposing (..) import Html.Attributes exposing (..) @@ -12,4 +12,4 @@ onSubmitPrevDefault value = onWithOptions "submit" { defaultOptions | preventDefault = True } - (Json.succeed value) + (Decode.succeed value) diff --git a/src/client/elm/View/Form.elm b/src/client/elm/View/Form.elm index dcde47d..7a4965d 100644 --- a/src/client/elm/View/Form.elm +++ b/src/client/elm/View/Form.elm @@ -1,5 +1,7 @@ module View.Form exposing ( textInput + , colorInput + , selectInput , radioInputs , hiddenSubmit ) @@ -13,7 +15,7 @@ import View.Color as Color import Form exposing (Form, FieldState) import Form.Input as Input -import Form.Error as FormError exposing (Error(..)) +import Form.Error as FormError exposing (ErrorValue(..)) import Form.Field as Field import Msg exposing (Msg) @@ -27,6 +29,7 @@ import Utils.Maybe exposing (isJust) textInput : Translations -> Form String a -> String -> String -> Html Form.Msg textInput translations form formName fieldName = let field = Form.getFieldAsString fieldName form + fieldId = formName ++ fieldName in div [ classList [ ("textInput", True) @@ -35,21 +38,39 @@ textInput translations form formName fieldName = ] [ Input.textInput field - [ id (formName ++ fieldName) + [ id fieldId , classList [ ("filled", isJust field.value) ] + , value (Maybe.withDefault "" field.value) ] , label - [ for (formName ++ fieldName) ] - [ text (Translations.getMessage translations (formName ++ fieldName)) ] + [ for fieldId ] + [ text (Translations.getMessage translations fieldId) ] , button - [ type' "button" - , onClick (Form.Input fieldName Field.EmptyField) + [ type_ "button" + , onClick (Form.Input fieldName Form.Text Field.EmptyField) , tabindex -1 ] [ FontAwesome.times Color.silver 15 ] - , case field.liveError of - Just error -> formError translations error - Nothing -> text "" + , formError translations field + ] + +colorInput : Translations -> Form String a -> String -> String -> Html Form.Msg +colorInput translations form formName fieldName = + let field = Form.getFieldAsString fieldName form + in div + [ classList + [ ("colorInput", True) + , ("error", isJust field.liveError) + ] + ] + [ label + [ for (formName ++ fieldName) ] + [ text (Translations.getMessage translations (formName ++ fieldName)) ] + , Input.textInput + field + [ id (formName ++ fieldName) + , type_ "color" + ] ] radioInputs : Translations -> Form String a -> String -> String -> List String -> Html Form.Msg @@ -67,9 +88,7 @@ radioInputs translations form formName radioName fieldNames = , div [ class "radioInputs" ] (List.map (radioInput translations field formName) fieldNames) - , case field.liveError of - Just error -> formError translations error - Nothing -> text "" + , formError translations field ] radioInput : Translations -> FieldState String String -> String -> String -> Html Form.Msg @@ -89,17 +108,41 @@ radioInput translations field formName fieldName = ] ] -formError : Translations -> FormError.Error String -> Html msg -formError translations error = - let errorElement error params = - div - [ class "errorMessage" ] - [ text (Translations.getParamMessage params translations error) ] - in case error of - CustomError key -> errorElement key [] - SmallerIntThan n -> errorElement "SmallerIntThan" [toString n] - GreaterIntThan n -> errorElement "GreaterIntThan" [toString n] - error -> errorElement (toString error) [] +selectInput : Translations -> Form String a -> String -> String -> List (String, String) -> Html Form.Msg +selectInput translations form formName selectName options = + let field = Form.getFieldAsString selectName form + fieldId = formName ++ selectName + in div + [ classList + [ ("selectInput", True) + , ("error", isJust field.liveError) + ] + ] + [ label + [ for fieldId ] + [ text (Translations.getMessage translations fieldId) ] + , Input.selectInput + (("", "") :: options) + field + [ id fieldId ] + , formError translations field + ] + +formError : Translations -> FieldState String a -> Html msg +formError translations field = + case field.liveError of + Just error -> + let errorElement error params = + div + [ class "errorMessage" ] + [ text (Translations.getParamMessage params translations error) ] + in case error of + CustomError key -> errorElement key [] + SmallerIntThan n -> errorElement "SmallerIntThan" [toString n] + GreaterIntThan n -> errorElement "GreaterIntThan" [toString n] + error -> errorElement (toString error) [] + Nothing -> + text "" hiddenSubmit : msg -> Html msg hiddenSubmit msg = diff --git a/src/client/elm/View/Header.elm b/src/client/elm/View/Header.elm index 08a429f..12fb87c 100644 --- a/src/client/elm/View/Header.elm +++ b/src/client/elm/View/Header.elm @@ -37,6 +37,7 @@ view model = LoggedInView { me, users } -> [ item Home "PaymentsTitle" , item Income "Income" + , item Categories "Categories" , item Statistics "Statistics" , div [ class "nameSignOut" ] -- cgit v1.2.3