jon/elm/Main.elm
2022-12-08 18:06:15 +01:00

358 lines
12 KiB
Elm

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)