module Entry exposing (main) import Browser import Http import Json.Decode as D import Json.Decode.Pipeline as P import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (..) import Calculator import NumberInput import Select main = Browser.element { init = \globals -> ( Context globals <| ItemSearch { searchTerm = "", searchResults = [] } , Cmd.none ) , subscriptions = \_ -> Sub.none , update = update , view = view } -- Data types type alias SearchResult = { barcode : String , name : String , netUnitPrice : Float , bought : String , salesUnits : Int , available : Bool , locationName : String , locationId : Int , groupName : String , groupId : Int , taxGroupId : Int } searchResultDecoder = D.succeed SearchResult |> P.required "item_barcode" D.string |> P.required "name" D.string |> P.required "unit_price" D.float |> P.required "bought" D.string |> P.required "sales_units" D.int |> P.required "available" D.bool |> P.required "location_name" D.string |> P.required "location_id" D.int |> P.required "group_name" D.string |> P.required "group_id" D.int |> P.required "tax_group_id" D.int type alias Location = { id : Int , name : String } type alias Group = { id : Int , name : String } type alias TaxGroup = { id : Int , description : String , percentage : Float } type alias NewItem = { barcode : String , name : String , salesUnits : Int , group : Group , location : Location , netUnitPrice : Float , taxGroup : TaxGroup } type alias Context = { globals : Globals , state : State } type alias Globals = { locations : List Location , groups : List Group , taxGroups : List TaxGroup } type State = ItemSearch { searchTerm : String , searchResults : List SearchResult } | ItemEditor { barcode : String , name : String , salesUnits : NumberInput.Model Int , calculator : Calculator.Model , netUnitPrice : NumberInput.Model Float , grossUnitPrice : NumberInput.Model Float , group : Select.Model Group , location : Select.Model Location , taxGroup : Select.Model TaxGroup } type Msg = SetSearchTerm String | SubmitSearch | ReceiveSearchResults (Result Http.Error (List SearchResult)) | GotoItemEditor SearchResult | SetBarcode String | SetName String | SetSalesUnits String | CalculatorMsg Calculator.Msg | SetNetUnitPrice String | SetGrossUnitPrice String | SetGroup String | SetLocation String | SetTaxGroup String -- Update logic: State machine etc. update msg { globals, state } = let (state_, cmd) = updateState msg globals state in ({ globals = globals, state = state_ }, cmd) updateState msg globals state = case state of ItemSearch model -> case msg of SetSearchTerm searchTerm -> (ItemSearch { model | searchTerm = searchTerm }, Cmd.none) SubmitSearch -> ( state , Http.get { url = "/entry/api/search-items?search-term=" ++ model.searchTerm , expect = Http.expectJson ReceiveSearchResults <| D.list searchResultDecoder } ) ReceiveSearchResults (Ok searchResults) -> (ItemSearch { model | searchResults = searchResults }, Cmd.none) GotoItemEditor searchResult -> case find (\tg -> tg.id == searchResult.taxGroupId) globals.taxGroups of Nothing -> (state, Cmd.none) Just taxGroup -> ( ItemEditor { barcode = searchResult.barcode , name = searchResult.name , calculator = Calculator.init searchResult.netUnitPrice , netUnitPrice = NumberInput.fromFloat searchResult.netUnitPrice , grossUnitPrice = NumberInput.fromFloat (suggestedGrossPrice searchResult.netUnitPrice taxGroup.percentage) , salesUnits = NumberInput.fromInt searchResult.salesUnits , group = Select.init (.id >> String.fromInt) (.name) { id = searchResult.groupId, name = searchResult.groupName } globals.groups , location = Select.init (.id >> String.fromInt) (.name) { id = searchResult.locationId, name = searchResult.locationName } globals.locations , taxGroup = Select.init (.id >> String.fromInt) (.description) taxGroup globals.taxGroups } , Cmd.none ) _ -> (state, Cmd.none) ItemEditor model -> case msg of SetBarcode barcode -> (ItemEditor { model | barcode = barcode }, Cmd.none) SetName name -> (ItemEditor { model | name = name }, Cmd.none) SetSalesUnits str -> (ItemEditor { model | salesUnits = NumberInput.update str model.salesUnits }, Cmd.none) CalculatorMsg msg_ -> (ItemEditor { model | calculator = Calculator.update msg_ model.calculator }, Cmd.none) SetNetUnitPrice str -> ( ItemEditor { model | netUnitPrice = NumberInput.update str model.netUnitPrice } , Cmd.none ) SetGrossUnitPrice str -> ( ItemEditor { model | grossUnitPrice = NumberInput.update str model.grossUnitPrice } , Cmd.none ) SetGroup key -> (ItemEditor { model | group = Select.update key model.group }, Cmd.none) SetLocation key -> (ItemEditor { model | location = Select.update key model.location }, Cmd.none) SetTaxGroup key -> ( ItemEditor { model | taxGroup = Select.update key model.taxGroup } , Cmd.none ) _ -> (state, Cmd.none) suggestedGrossPrice netPrice percentage = roundTo 2 <| netPrice * (1 + percentage) + 0.01 -- View stuff view { globals, state } = case state of ItemSearch model -> fieldset [] [ legend [] [ text "Vorlage für neuen Inventareintrag" ] , Html.form [ onSubmit SubmitSearch ] [ div [ class "form-input" ] [ label [ for "search-term", title "Barcode oder Name" ] [ text "Suchbegriff" ] , input [ onInput SetSearchTerm, value model.searchTerm, id "search-term" ] [] ] , table [] <| searchResultHeaders :: List.map viewSearchResult model.searchResults ] ] ItemEditor model -> Html.form [ method "POST" ] [ fieldset [] [ legend [] [ text "Neuer Inventareintrag" ] , div [ class "form-input" ] [ label [ for "barcode" ] [ text "Barcode" ] , input [ onInput SetBarcode, value model.barcode, name "barcode", id "barcode" ] [] ] , div [ class "form-input" ] [ label [ for "name" ] [ text "Name" ] , input [ onInput SetName, value model.name, name "name", id "name" ] [] ] , div [ class "form-input" ] [ label [ for "sales-units" ] [ text "Eingekauft" ] , input [ onInput SetSalesUnits, value <| NumberInput.show model.salesUnits, name "sales-units", id "sales-units", type_ "number" ] [] ] , div [ class "form-input" ] [ label [ for "group" ] [ text "Gruppe" ] , Select.view [ name "group", id "group" ] SetGroup model.group ] , div [ class "form-input" ] [ label [ for "location" ] [ text "Raum" ] , Select.view [ name "location", id "location" ] SetLocation model.location ] , div [ class "form-input" ] [ label [ for "tax-group" ] [ text "Steuergruppe" ] , Select.view [ name "tax-group", id "tax-group" ] SetTaxGroup model.taxGroup ] , Html.map CalculatorMsg <| Calculator.view model.calculator (Select.get model.taxGroup) , div [ class "form-input" ] [ label [ for "net-unit-price" ] [ text "Einkaufspreis (Netto)" ] , input [ value <| NumberInput.show model.netUnitPrice , onInput SetNetUnitPrice , type_ "number" , name "net-unit-price" , id "net-unit-price" , step "0.01" ] [] , viewSetCalculatedPriceButton model ] , div [ class "form-input" ] [ label [ for "gross-unit-price" ] [ text "Verkaufspreis (Brutto)" ] , input [ value <| NumberInput.show model.grossUnitPrice , onInput SetGrossUnitPrice , type_ "number" , name "gross-unit-price" , id "gross-unit-price" , step "0.01" ] [] , viewSetSuggestedPriceButton model ] , button [] [ text "Auftrag anlegen" ] ] ] viewSetCalculatedPriceButton model = case Calculator.getResult model.calculator (Select.get model.taxGroup) of Nothing -> button [ disabled True ] [ text "Auf ? setzen" ] Just calculatedPrice -> button [ onClick <| SetNetUnitPrice <| String.fromFloat calculatedPrice -- Prevent submitting the form , type_ "button" ] [ text <| "Auf " ++ String.fromFloat calculatedPrice ++ " setzen" ] viewSetSuggestedPriceButton model = case NumberInput.get model.netUnitPrice of Nothing -> button [ disabled True ] [ text "Auf ? setzen" ] Just netUnitPrice -> let grossUnitPrice = suggestedGrossPrice netUnitPrice (Select.get model.taxGroup).percentage in button [ onClick <| SetGrossUnitPrice <| String.fromFloat grossUnitPrice -- Prevent submitting the form , type_ "button" ] [ text <| "Auf " ++ String.fromFloat grossUnitPrice ++ " setzen" ] searchResultHeaders = tr [] [ th [] [ text "Barcode" ] , th [] [ text "Name" ] , th [] [ text "Gruppe" ] , th [] [ text "Stückpreis (Netto)" ] , th [] [ text "Eingekauft" ] , th [] [ text "Kaufdatum" ] , th [] [ text "Raum" ] , th [] [ text "Aktiv?" ] , th [] [] ] viewSearchResult model = tr [] [ td [] [ code [] [ text model.barcode ] ] , td [] [ text model.name ] , td [] [ text model.groupName ] , td [] [ text <| String.fromFloat model.netUnitPrice ] , td [] [ text <| String.fromInt model.salesUnits ] , td [] [ text model.bought ] , td [] [ text model.locationName ] , td [] [ text <| showBool model.available ] , td [] [ button [ onClick <| GotoItemEditor model ] [ text "Als Vorlage verwenden" ] ] ] calculateGarfieldPrice model = NumberInput.get model.netUnitPrice |> Maybe.map (\netUnitPrice -> roundTo 2 <| netUnitPrice * (1 + (Select.get model.taxGroup).percentage) + 0.01 ) roundTo places x = toFloat (round <| x * 10 ^ places) / 10 ^ places showBool b = case b of True -> "✅" False -> "❌" find pred xs = List.head <| List.filter pred xs