module Main exposing (..) import Browser import Dict exposing (Dict) import Html exposing (..) import Html.Attributes exposing ( checked , disabled , style , type_ , value ) import Html.Events exposing (..) import Http import Json.Decode as Dec import Json.Decode.Pipeline exposing (..) import Json.Encode as Enc import Set exposing (Set) main = Browser.element { init = \() -> init , subscriptions = \_ -> Sub.none , update = update , view = view } getOverviewItems : Int -> Cmd Msg getOverviewItems location = Http.post { url = "/rpc/getOverviewItems" , body = Http.jsonBody <| Enc.object [ ("location", Enc.int location) ] , expect = Http.expectJson RcvOverview <| Dec.list decodeOI } rpc : { func : String, args : Enc.Value, expect : Dec.Decoder a } -> (Result Http.Error a -> b) -> Cmd b rpc { func, args, expect } mkMsg = Http.post { url = "/rpc/" ++ func , body = Http.jsonBody args , expect = Http.expectJson mkMsg expect } getLocations : Cmd Msg getLocations = rpc { func = "getLocations" , args = Enc.object [] , expect = Dec.list (Dec.succeed Location |> required "id" Dec.int |> required "name" Dec.string) } RcvLocations adjustInventory : Int -> Int -> String -> Cmd Msg adjustInventory itemId adjustment description = Http.post { url = "/rpc/adjustInventory" , body = Http.jsonBody <| Enc.object [ ("item", Enc.int itemId) , ("amount", Enc.int adjustment) , ("description", Enc.string description) ] , expect = Http.expectWhatever (\_ -> RcvOther) } type alias InventoryTransferDTO = { from : Int, to : Int, amount : Int } transferInventory : List InventoryTransferDTO -> Cmd Msg transferInventory transfers = Http.post { url = "/rpc/transferInventory" , body = Http.jsonBody (Enc.object [ ("transfers", Enc.list encodeTransfer transfers) ]) , expect = Http.expectWhatever (\_ -> RcvOther) } encodeTransfer t = Enc.object [ ("from", Enc.int t.from) , ("to", Enc.int t.to) , ("amount", Enc.int t.amount) ] disableItems : List Int -> Cmd Msg disableItems ids = Http.post { url = "/rpc/disableItems" , body = Http.jsonBody (Enc.object [ ("items", Enc.list Enc.int ids) ]) , expect = Http.expectWhatever (\_ -> RcvOther) } decodeOI = Dec.succeed OverviewItem |> requiredAt ["item", "id"] Dec.int |> requiredAt ["item", "barcode"] Dec.string |> requiredAt ["item", "name"] Dec.string |> requiredAt ["overview", "unitsLeft"] Dec.int |> requiredAt ["item", "unitPrice"] Dec.float |> requiredAt ["item", "bought"] Dec.string |> requiredAt ["overview", "activeMappings"] Dec.int getSnacksByItemId : Int -> Cmd Msg getSnacksByItemId itemId = rpc { func = "getSnacksByItemId" , args = Enc.object [ ("item", Enc.int itemId) ] , expect = Dec.list decodeSnack } RcvSnacks type alias Snack = { id : Int , name : String , barcode : String , price : Float , location : Int , taxGroup : Int } decodeSnack = Dec.succeed Snack |> required "id" Dec.int |> required "name" Dec.string |> required "barcode" Dec.string |> required "price" Dec.float |> required "location" Dec.int |> required "taxGroup" Dec.int type alias OverviewItem = { id : Int , barcode : String , name : String , unitsLeft : Int , price : Float , bought : String , activeMappings : Int } type alias Location = { id : Int , name : String } type alias Model = { state : State } type State = LoadingLocations | LocationSelector (List Location) | Overview { location : Location , selectedItems : Set Int , desiredInventory : Dict Int Int , overviewItems : List OverviewItem } | SnacksEditor { snacks : List Snack } type Msg = SelectItem Int Bool | SetDesiredInventory Int String | SelectLocation Location | TransferInventory Int -- RPC calls | CallDisableItems (List Int) | CallAdjustInventory Int Int String | CallGetSnacksById Int -- Responses | RcvLocations (Result Http.Error (List Location)) | RcvOverview (Result Http.Error (List OverviewItem)) | RcvSnacks (Result Http.Error (List Snack)) | RcvOther init = ({ state = LoadingLocations }, getLocations) update msg global = case msg of CallAdjustInventory item amount desc -> (global, adjustInventory item amount desc) CallDisableItems items -> (global, disableItems items) CallGetSnacksById itemId -> (global, getSnacksByItemId itemId) _ -> let (newState, cmd) = stateMachine msg global global.state in ({ global | state = newState }, cmd) stateMachine msg global state = case state of LoadingLocations -> case msg of RcvLocations (Ok locations) -> (LocationSelector locations, Cmd.none) _ -> (state, Cmd.none) LocationSelector locations -> case msg of SelectLocation location -> (Overview { location = location , selectedItems = Set.empty , desiredInventory = Dict.empty , overviewItems = [] } , getOverviewItems location.id ) _ -> (state, Cmd.none) Overview model -> case msg of RcvOverview (Ok overviewItems) -> (Overview { location = model.location , selectedItems = Set.empty , desiredInventory = Dict.empty , overviewItems = overviewItems } , Cmd.none ) RcvSnacks (Ok snacks) -> (SnacksEditor { snacks = snacks }, Cmd.none) RcvOther -> (state, getOverviewItems model.location.id) SelectItem itemId selected -> (Overview { model | selectedItems = setSelect itemId selected model.selectedItems }, Cmd.none) SetDesiredInventory itemId invStr -> case String.toInt invStr of Just inv -> (Overview { model | desiredInventory = Dict.insert itemId inv model.desiredInventory }, Cmd.none) Nothing -> (state, Cmd.none) TransferInventory targetId -> let transfers = model.overviewItems |> List.filterMap (\oi -> if Set.member oi.id model.selectedItems then Just { from = oi.id, to = targetId, amount = oi.unitsLeft } else Nothing) in (state, transferInventory transfers) _ -> (state, Cmd.none) SnacksEditor { snacks } -> (state, Cmd.none) view { state } = case state of LoadingLocations -> progress [] [] LocationSelector locations -> let viewLocationButton location = button [ onClick <| SelectLocation location ] [ text location.name ] in div [] [ p [] [ text "Raum auswählen:" ] , div [] <| List.map viewLocationButton locations ] Overview { location, selectedItems, desiredInventory, overviewItems } -> let header = tableCells th <| List.map text [ "", "ID", "Artikel", "Barcode", "Preis", "Kaufdatum", "Snackeinträge", "Soll-Inv.", "Ist-Inv.", "Aktionen" ] viewOverviewItem oi = let adjustedInventory = Maybe.withDefault oi.unitsLeft <| Dict.get oi.id desiredInventory mkAdjustInventoryMsg itemId adjustment = if adjustment > 0 then CallAdjustInventory itemId adjustment "Gewinn" else CallAdjustInventory itemId adjustment "Verlust" viewAdjustedInventory adjustment = if adjustment == 0 then "" else if adjustment > 0 then "(+" ++ String.fromInt adjustment ++ ")" else "(" ++ String.fromInt adjustment ++ ")" sumSelected = overviewItems |> List.filter (\x -> Set.member x.id selectedItems) |> List.map .unitsLeft |> List.sum in tableCells td [ input [ type_ "checkbox" , onCheck <| SelectItem oi.id , checked <| Set.member oi.id selectedItems ] [] , text <| String.fromInt oi.id , text oi.name , code [] [ text oi.barcode ] , text <| String.fromFloat oi.price , text <| Tuple.first <| splitAt 'T' oi.bought , text <| String.fromInt oi.activeMappings , text <| String.fromInt oi.unitsLeft , input [ type_ "number" , onInput <| SetDesiredInventory oi.id , value <| String.fromInt adjustedInventory , style "width" "5em" ] [] , span [] [ button (if adjustedInventory == oi.unitsLeft then [ disabled True ] else [ onClick <| mkAdjustInventoryMsg oi.id <| adjustedInventory - oi.unitsLeft ]) [ text <| "Inventar korrigieren" ++ viewAdjustedInventory (adjustedInventory - oi.unitsLeft) ] , button (if oi.activeMappings /= 0 || oi.unitsLeft /= 0 then [ disabled True ] else [ onClick <| CallDisableItems [oi.id] ]) [ text "Eintrag deaktivieren" ] , button (if Set.member oi.id selectedItems || sumSelected == 0 then [ disabled True ] else [ onClick <| TransferInventory oi.id ]) [ text <| String.fromInt sumSelected ++ " Einheiten umbuchen" ] , button (if oi.activeMappings == 0 then [ disabled True ] else [ onClick <| CallGetSnacksById oi.id ]) [ text "Snackeinträge bearbeiten" ] ] ] in div [] [ h2 [] [ text <| "Inventar " ++ location.name ] , table [] <| [header] ++ List.map viewOverviewItem overviewItems ] SnacksEditor { snacks } -> let header = tableCells th <| List.map text [ "ID", "Artikel", "Barcode", "Brutto" ] viewSnack snack = tableCells td [ text <| String.fromInt snack.id , text snack.name , text snack.barcode , text <| String.fromFloat snack.price ] in table [] [ thead [] [ header ] , tbody [] <| List.map viewSnack snacks ] -- utils tableCells f = let mkTd elem = f [] [ elem ] in tr [] << List.map mkTd setSelect elem state = (if state then Set.insert else Set.remove) elem splitAt : Char -> String -> (String, String) splitAt delim str = let locate c s = case String.uncons s of Nothing -> 0 Just (x, xs) -> if x == c then 0 else 1 + locate c xs firstOcc = locate delim str in (String.slice 0 firstOcc str, String.slice firstOcc (String.length str) str)