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 (..) 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 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 : Int , netUnitPrice : Float , groupId : Int , locationId : Int , taxGroupId : Int } type Msg = SetSearchTerm String | SubmitSearch | ReceiveSearchResults (Result Http.Error (List SearchResult)) | GotoItemEditor SearchResult | SetNetUnitPrice String | SetGroupId String | SetLocationId String | SetTaxGroupId String | SetBarcode String | SetName String | SetSalesUnits 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 -> ( ItemEditor { barcode = searchResult.barcode , groupId = searchResult.groupId , locationId = searchResult.locationId , name = searchResult.name , netUnitPrice = searchResult.netUnitPrice , salesUnits = searchResult.salesUnits , taxGroupId = searchResult.taxGroupId } , Cmd.none ) _ -> (state, Cmd.none) ItemEditor model -> case msg of SetNetUnitPrice netUnitPriceStr -> case String.toFloat netUnitPriceStr of Nothing -> (state, Cmd.none) Just netUnitPrice -> (ItemEditor { model | netUnitPrice = netUnitPrice }, Cmd.none) SetGroupId groupIdStr -> case String.toInt groupIdStr of Nothing -> (state, Cmd.none) Just groupId -> (ItemEditor { model | groupId = groupId }, Cmd.none) SetLocationId locationIdStr -> case String.toInt locationIdStr of Nothing -> (state, Cmd.none) Just locationId -> (ItemEditor { model | locationId = locationId }, Cmd.none) SetTaxGroupId taxGroupIdStr -> case String.toInt taxGroupIdStr of Nothing -> (state, Cmd.none) Just taxGroupId -> (ItemEditor { model | taxGroupId = taxGroupId }, Cmd.none) SetBarcode barcode -> (ItemEditor { model | barcode = barcode }, Cmd.none) SetName name -> (ItemEditor { model | name = name }, Cmd.none) SetSalesUnits salesUnitsStr -> case String.toInt salesUnitsStr of Nothing -> (state, Cmd.none) Just salesUnits -> (ItemEditor { model | salesUnits = salesUnits }, Cmd.none) _ -> (state, Cmd.none) -- 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 -> case find (\tg -> tg.id == model.taxGroupId) globals.taxGroups of Nothing -> div [] [ text "index error, this should never happen" ] Just selectedTaxGroup -> Html.form [] [ fieldset [] [ legend [] [ text "Neuer Inventareintrag" ] , div [ class "form-input" ] [ label [ for "barcode" ] [ text "Barcode" ] , input [ onInput SetBarcode, value model.barcode, disabled True, id "barcode" ] [] ] , div [ class "form-input" ] [ label [ for "name" ] [ text "Name" ] , input [ onInput SetName, value model.name, id "name" ] [] ] , div [ class "form-input" ] [ label [ for "sales-units" ] [ text "Stückzahl" ] , input [ onInput SetSalesUnits, value <| String.fromInt model.salesUnits, id "sales-units", type_ "number" ] [] ] , div [ class "form-input" ] [ label [ for "group" ] [ text "Gruppe" ] , viewSelect [ onInput SetGroupId, id "group" ] String.fromInt .id .name model.groupId globals.groups ] , div [ class "form-input" ] [ label [ for "location" ] [ text "Raum" ] , viewSelect [ onInput SetLocationId, id "location" ] String.fromInt .id .name model.locationId globals.locations ] , div [ class "form-input" ] [ label [ for "net-unit-price" ] [ text "Stückpreis (Netto)" ] , input [ value <| String.fromFloat model.netUnitPrice , onInput SetNetUnitPrice , type_ "number" , id "net-unit-price" , step "0.01" ] [] ] , div [ class "form-input" ] [ label [ for "tax-group" ] [ text "Steuergruppe" ] , viewSelect [ onInput SetTaxGroupId, id "tax-group" ] String.fromInt .id .description model.taxGroupId globals.taxGroups ] ] , fieldset [] [ legend [] [ text "Neuer Snackeintrag" ] , div [ class "form-input" ] [ label [ for "snack-name" ] [ text "Name" ] , input [ value model.name, disabled True, id "snack-name" ] [] ] , div [ class "form-input" ] [ label [ for "gross-unit-price" ] [ text <| "Stückpreis (Brutto), Vorschlag: " ++ String.fromFloat (calculateGarfieldPrice model.netUnitPrice selectedTaxGroup.percentage) ] , input [ value <| String.fromFloat <| Maybe.withDefault (calculateGarfieldPrice model.netUnitPrice selectedTaxGroup.percentage) Nothing, id "gross-unit-price", type_ "number", step "0.01" ] [] ] ] ] viewSelect selectAttributes showValue getValue getLabel selectedValue xs = let viewOption x = option [ value <| showValue <| getValue x , selected <| getValue x == selectedValue ] [ text <| getLabel x ] in select selectAttributes <| List.map viewOption xs 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 netUnitPrice taxPercentage = roundTo 2 <| netUnitPrice * (1 + taxPercentage) + 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