337 lines
10 KiB
Elm
337 lines
10 KiB
Elm
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 []
|
|
[ 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 <| NumberInput.show model.salesUnits, id "sales-units", type_ "number" ] []
|
|
]
|
|
, div [ class "form-input" ]
|
|
[ label [ for "group" ] [ text "Gruppe" ]
|
|
, Select.view SetGroup model.group
|
|
]
|
|
, div [ class "form-input" ]
|
|
[ label [ for "location" ] [ text "Raum" ]
|
|
, Select.view SetLocation model.location
|
|
]
|
|
, div [ class "form-input" ]
|
|
[ label [ for "tax-group" ] [ text "Steuergruppe" ]
|
|
, Select.view 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"
|
|
, 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"
|
|
, id "gross-unit-price"
|
|
, step "0.01"
|
|
]
|
|
[]
|
|
, viewSetSuggestedPriceButton model
|
|
]
|
|
]
|
|
]
|
|
|
|
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
|