Compare commits

...

5 Commits

Author SHA1 Message Date
fa2d060143 Fix typo 2022-12-13 18:46:10 +01:00
26b087c041 Add inventory line view 2022-12-13 18:41:39 +01:00
f02c7462f3 Add title 2022-12-11 01:24:23 +01:00
05724dbaab Restructure client code 2022-12-08 22:42:10 +01:00
9ce0f974fb Add a few things 2022-12-08 18:06:15 +01:00
6 changed files with 497 additions and 137 deletions

View File

@ -1,8 +1,15 @@
module Main exposing (..) module Main exposing (..)
import Browser import Browser
import Dict exposing (Dict)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing
( checked
, disabled
, style
, type_
, value
)
import Html.Events exposing (..) import Html.Events exposing (..)
import Http import Http
import Json.Decode as Dec import Json.Decode as Dec
@ -10,40 +17,58 @@ import Json.Decode.Pipeline exposing (..)
import Json.Encode as Enc import Json.Encode as Enc
import Set exposing (Set) import Set exposing (Set)
main = Browser.element main = Browser.document
{ init = \() -> (init, getUnsoundBarcodes loc) { init = \() -> init
, subscriptions = \_ -> Sub.none , subscriptions = \_ -> Sub.none
, update = update , update = update
, view = view , view = \outerState -> { title = "jon", body = [ view outerState ] }
} }
getUnsoundBarcodes : Int -> Cmd Msg getOverviewItems : Int -> Cmd Msg
getUnsoundBarcodes location = Http.post getOverviewItems location = Http.post
{ url = "/rpc/getUnsoundBarcodes" { url = "/rpc/getOverviewItems"
, body = Http.jsonBody (Enc.object [("location", Enc.int location)]) , body = Http.jsonBody <| Enc.object
, expect = Http.expectJson RcvUnsoundBarcodes <| Dec.list (Dec.map3 UnsoundBarcode [ ("location", Enc.int location)
(Dec.field "barcode" Dec.string) ]
(Dec.field "name" Dec.string) , expect = Http.expectJson RcvOverview <| Dec.list decodeOI
(Dec.field "entries" Dec.int))
} }
getActiveItems : String -> Int -> Cmd Msg rpc : { func : String, args : Enc.Value, expect : Dec.Decoder a } -> (Result Http.Error a -> b) -> Cmd b
getActiveItems barcode location = Http.post rpc { func, args, expect } mkMsg = Http.post
{ url = "/rpc/getActiveItems" { url = "/rpc/" ++ func
, body = Http.jsonBody (Enc.object , body = Http.jsonBody args
[ ("barcode", Enc.string barcode) , expect = Http.expectJson mkMsg expect
, ("location", Enc.int location)
])
, expect = Http.expectJson (RcvActiveItems barcode) <| Dec.list decodeOI
} }
transferInventory : List { from : Int, to : Int, amount : Int } -> Cmd Msg 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 transferInventory transfers = Http.post
{ url = "/rpc/transferInventory" { url = "/rpc/transferInventory"
, body = Http.jsonBody (Enc.object , body = Http.jsonBody (Enc.object
[ ("transfers", Enc.list encodeTransfer transfers) [ ("transfers", Enc.list encodeTransfer transfers)
]) ])
, expect = Http.expectWhatever RcvTransferResponse , expect = Http.expectWhatever (\_ -> RcvOther)
} }
encodeTransfer t = Enc.object encodeTransfer t = Enc.object
@ -58,7 +83,16 @@ disableItems ids = Http.post
, body = Http.jsonBody (Enc.object , body = Http.jsonBody (Enc.object
[ ("items", Enc.list Enc.int ids) [ ("items", Enc.list Enc.int ids)
]) ])
, expect = Http.expectWhatever RcvDisableItemResponse , expect = Http.expectWhatever (\_ -> RcvOther)
}
deleteSnack : Int -> Cmd Msg
deleteSnack snack = Http.post
{ url = "/rpc/deleteSnack"
, body = Http.jsonBody (Enc.object
[ ("snack", Enc.int snack)
])
, expect = Http.expectWhatever (\_ -> RcvOther)
} }
decodeOI = Dec.succeed OverviewItem decodeOI = Dec.succeed OverviewItem
@ -69,13 +103,35 @@ decodeOI = Dec.succeed OverviewItem
|> requiredAt ["item", "unitPrice"] Dec.float |> requiredAt ["item", "unitPrice"] Dec.float
|> requiredAt ["item", "bought"] Dec.string |> requiredAt ["item", "bought"] Dec.string
|> requiredAt ["overview", "activeMappings"] Dec.int |> requiredAt ["overview", "activeMappings"] Dec.int
|> requiredAt ["item", "group"] Dec.int
|> requiredAt ["overview", "groupName"] Dec.string
type alias UnsoundBarcode = getSnacksByItem : OverviewItem -> Cmd Msg
{ barcode : String getSnacksByItem item = rpc
{ func = "getSnacksByItemId"
, args = Enc.object
[ ("item", Enc.int item.id)
]
, expect = Dec.list decodeSnack
} (RcvSnacks item)
type alias Snack =
{ id : Int
, name : String , name : String
, entries : Int , 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 = type alias OverviewItem =
{ id : Int { id : Int
, barcode : String , barcode : String
@ -84,118 +140,286 @@ type alias OverviewItem =
, price : Float , price : Float
, bought : String , bought : String
, activeMappings : Int , activeMappings : Int
, groupId : Int
, groupName : String
} }
type Model type alias Location =
= Init { id : Int
| UBList (List UnsoundBarcode) , name : String
| Overview String (Set Int) (List OverviewItem) }
type OuterState
= LoadingLocations
| LocationSelector (List Location)
| Initialized { locations : List Location, location : Location } State
type State
= Overview
{ selectedItems : Set Int
, desiredInventory : Dict Int Int
, overviewItems : List OverviewItem
}
| ViewingItem
{ item : OverviewItem
, snacks : List Snack
}
type Msg type Msg
= RcvUnsoundBarcodes (Result Http.Error (List UnsoundBarcode)) = SelectLocation Location
| GetActiveItems String Int | SelectItem Int Bool
| RcvActiveItems String (Result Http.Error (List OverviewItem)) | ChangeLocation
| SetSelected Int Bool | SetDesiredInventory Int String
| TransferInventory (List { amount : Int, from : Int, to : Int }) | TransferInventory Int
| RcvTransferResponse (Result Http.Error ())
| DisableItem Int
| RcvDisableItemResponse (Result Http.Error ())
| GoBack | GoBack
-- RPC calls
| CallDisableItems (List Int)
| CallAdjustInventory Int Int String
| CallGetSnacksById OverviewItem
| CallDeleteSnack Int
-- Responses
| RcvLocations (Result Http.Error (List Location))
| RcvOverview (Result Http.Error (List OverviewItem))
| RcvSnacks OverviewItem (Result Http.Error (List Snack))
| RcvOther
init = Init init = (LoadingLocations, getLocations)
loc = 2
update msg model = case model of update msg outerState = case msg of
Init -> case msg of CallAdjustInventory item amount desc -> (outerState, adjustInventory item amount desc)
RcvUnsoundBarcodes (Ok ubs) -> (UBList ubs, Cmd.none) CallDisableItems items -> (outerState, disableItems items)
_ -> (model, Cmd.none) CallGetSnacksById item -> (outerState, getSnacksByItem item)
UBList _ -> case msg of CallDeleteSnack snack -> (outerState, deleteSnack snack)
GetActiveItems barcode location -> (model, getActiveItems barcode location) _ -> case outerState of
RcvActiveItems barcode (Ok ois) -> (Overview barcode (Set.empty) ois, Cmd.none) LoadingLocations -> case msg of
_ -> (model, Cmd.none) RcvLocations (Ok locations) ->
Overview barcode selectedItems ois -> case msg of (LocationSelector locations, Cmd.none)
SetSelected id checked -> (Overview barcode ((if checked then Set.insert else Set.remove) id selectedItems) ois, Cmd.none) _ ->
TransferInventory transfers -> (model, transferInventory transfers) (outerState, Cmd.none)
RcvTransferResponse _ -> (model, getActiveItems barcode loc) LocationSelector locations -> case msg of
RcvActiveItems barcode_ (Ok ois_) -> (Overview barcode_ (Set.empty) ois_, Cmd.none) SelectLocation location ->
DisableItem id -> (model, disableItems [id]) (Initialized { locations = locations, location = location } <| Overview
RcvDisableItemResponse _ -> (model, getActiveItems barcode loc) { selectedItems = Set.empty
GoBack -> (model, getUnsoundBarcodes loc) , desiredInventory = Dict.empty
RcvUnsoundBarcodes (Ok ubs) -> (UBList ubs, Cmd.none) , overviewItems = []
_ -> (model, Cmd.none) }
, getOverviewItems location.id
view model = case model of )
Init -> h1 [] [ text "It works!" ] _ ->
UBList ubs -> viewUBList ubs (outerState, Cmd.none)
Overview barcode selectedItems ois -> viewOverview barcode selectedItems ois Initialized global state -> case msg of
ChangeLocation ->
viewUBList ubs = (LocationSelector global.locations, Cmd.none)
_ ->
let let
header = tr [] (newState, cmd) = stateMachine msg global state
[ th [] [ text "Barcode" ] in
, th [] [ text "Artikel" ] (Initialized global newState, cmd)
, th [] [ text "Einträge" ]
, th [] [ text "Aktionen" ] stateMachine msg global state = case state of
Overview model -> case msg of
RcvOverview (Ok overviewItems) ->
(Overview
{ selectedItems = Set.empty
, desiredInventory = Dict.empty
, overviewItems = overviewItems
}
, Cmd.none
)
RcvSnacks item (Ok snacks) ->
(ViewingItem { item = item, snacks = snacks }, Cmd.none)
RcvOther ->
(state, getOverviewItems global.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)
ViewingItem { item } -> case msg of
GoBack ->
(Overview { selectedItems = Set.empty, desiredInventory = Dict.empty, overviewItems = [] }, getOverviewItems global.location.id)
RcvSnacks item_ (Ok snacks) ->
(ViewingItem { item = item_, snacks = snacks }, Cmd.none)
RcvOther ->
(state, getSnacksByItem item)
_ ->
(state, Cmd.none)
view outerState = case outerState 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
]
Initialized global state ->
div []
[ h2 []
[ text <| "Inventar " ++ global.location.name ++ " "
, button [ onClick ChangeLocation ] [ text "Raum ändern" ]
]
, viewState global state
]
viewState global state = case state of
Overview { selectedItems, desiredInventory, overviewItems } ->
let
header = tableCells th <| List.map text [ "", "ID", "Artikel", "EAN", "Preis", "Kaufdatum", "Snackeinträge", "Inventar", "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 <| showEuros oi.price
, text <| Tuple.first <| splitAt 'T' oi.bought
, text <| String.fromInt oi.activeMappings
, input
[ type_ "number"
, onInput <| SetDesiredInventory oi.id
, value <| String.fromInt adjustedInventory
, style "width" "5em"
] []
, div []
[ 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.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 [ onClick <| CallGetSnacksById oi ] [ text "Anzeigen" ]
] ]
viewUB ub = tr []
[ td [] [ text ub.barcode ]
, td [] [ text ub.name ]
, td [] [ text <| String.fromInt ub.entries ]
, td [] [ button [ onClick (GetActiveItems ub.barcode loc) ] [ text "Einträge ansehen" ] ]
] ]
in in
table [] ([header] ++ List.map viewUB ubs) div []
[ table [] <| [header] ++ List.map viewOverviewItem overviewItems
viewOverview : String -> Set Int -> List OverviewItem -> Html Msg ]
viewOverview barcode selectedItems ois = ViewingItem { item, snacks } ->
let let
sumSelected = header = tableCells th <| List.map text [ "ID", "Artikel", "Barcode", "Bruttoverkaufspreis", "Aktionen" ]
List.sum viewSnack snack = tableCells td
<| List.map (\oi -> oi.unitsLeft) [ text <| String.fromInt snack.id
<| List.filter (\oi -> Set.member oi.id selectedItems) ois , text snack.name
, code [] [ text snack.barcode ]
header = tr [] , text <| showEuros snack.price ++ " (+" ++ showEuros (roundTo 2 <| snack.price - item.price) ++ ")"
[ th [] [] , div []
, th [] [ text "ID" ] [ button [ onClick <| CallDeleteSnack snack.id ] [ text "Deaktivieren" ]
, th [] [ text "Barcode" ]
, th [] [ text "Artikel" ]
, th [] [ text "Kaufdatum" ]
, th [] [ text "Inventar (Soll)" ]
, th [] [ text "Preis" ]
, th [] [ text "Snackeinträge" ]
, th [] [ text "Aktionen" ]
] ]
viewOI oi = tr []
[ th [] [ input [ type_ "checkbox", onCheck <| SetSelected oi.id, selected <| Set.member oi.id selectedItems ] [] ]
, td [] [ text <| String.fromInt oi.id ]
, td [] [ text oi.barcode ]
, td [] [ text oi.name ]
, td [] [ text oi.bought ]
, td [] [ text <| String.fromInt oi.unitsLeft ]
, td [] [ text <| String.fromFloat oi.price ]
, td [] [ text <| String.fromInt oi.activeMappings ]
, td [] (viewButtons oi)
] ]
viewButtons oi = itemProp label value = tr []
[ button [ th [ style "text-align" "left" ] [ text label ]
[ disabled <| Set.member oi.id selectedItems || sumSelected == 0 , td [] value
, onClick <| mkTransferMessage selectedItems oi.id
] ]
[ text <| String.fromInt sumSelected ++ " Einträge umbuchen" ]
, button
[ disabled <| oi.unitsLeft /= 0 || oi.activeMappings /= 0
, onClick <| DisableItem oi.id
]
[ text "Eintrag deaktivieren" ]
]
mkTransferMessage fromIds toId =
TransferInventory
<| List.map (\oi -> { amount = oi.unitsLeft, from = oi.id, to = toId })
<| List.filter (\oi -> Set.member oi.id fromIds) ois
in in
div [] div []
[ button [ onClick GoBack ] [ text "Zurück" ] [ button [ onClick GoBack ] [ text "Zurück" ]
, h2 [] [ text barcode ] , fieldset []
, table [] ([header] ++ List.map viewOI ois) [ legend [] [ text <| "Inventareintrag " ++ String.fromInt item.id ]
, table []
[ tbody []
[ itemProp "ID" [ text <| String.fromInt item.id ]
, itemProp "EAN" [ code [] [ text item.barcode ] ]
, itemProp "Artikel" [ text item.name ]
, itemProp "Gruppe" [ text <| item.groupName ++ " (" ++ String.fromInt item.groupId ++ ")" ]
, itemProp "Inventar" [ text <| String.fromInt item.unitsLeft ]
, itemProp "Kaufdatum" [ text <| Tuple.first <| splitAt 'T' item.bought ]
, itemProp "Nettoeinkaufspreis" [ text <| showEuros item.price ]
] ]
]
]
, h3 [] [ text "Snacks" ]
, 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 + 1) (String.length str) str)
showEuros : Float -> String
showEuros x =
let
(whole, fractional) = splitAt '.' (String.fromFloat x)
in
whole ++ "," ++ String.slice 0 2 (fractional ++ "00") ++ ""
roundTo : Int -> Float -> Float
roundTo decimals x =
let
m = toFloat <| 10^decimals
in
toFloat (round (x * m)) / m

View File

@ -68,6 +68,17 @@ locations
:: Q Postgres GarfieldDb s (LocationT (QExpr Postgres s)) :: Q Postgres GarfieldDb s (LocationT (QExpr Postgres s))
locations = all_ garfieldDb.locations locations = all_ garfieldDb.locations
getSnacksByItemId
:: InventoryItemId
-> Q Postgres GarfieldDb s (SnackT (QExpr Postgres s))
getSnacksByItemId item = do
im <- all_ garfieldDb.inventoryMap
sn <- related_ garfieldDb.snacks im.snackId
sa <- oneToOne_ garfieldDb.snacksAvailable (.snackId) sn
guard_ $ im.inventoryId ==. val_ item
guard_ $ sa.available
pure sn
-- Inserts -- Inserts
runInserts runInserts
@ -76,6 +87,14 @@ runInserts
-> IO () -> IO ()
runInserts conn is = runBeamPostgresDebug putStrLn conn $ mapM_ runInsert is runInserts conn is = runBeamPostgresDebug putStrLn conn $ mapM_ runInsert is
adjustInventory
:: InventoryItemId
-> Int64
-> Text
-> SqlInsert Postgres CorrectionT
adjustInventory item amount desc = insert garfieldDb.inventoryCorrections $
insertExpressions [Correction (val_ item) default_ (val_ amount) (val_ desc)]
transfer transfer
:: InventoryItemId -- ^ to :: InventoryItemId -- ^ to
-> InventoryItemId -- ^ from -> InventoryItemId -- ^ from

View File

@ -20,6 +20,9 @@ module Jon.Garfield.Types
, InventoryItemGroup , InventoryItemGroup
, InventoryItemGroupId , InventoryItemGroupId
, InventoryItemGroupT(..) , InventoryItemGroupT(..)
, InventoryMap
, InventoryMapId
, InventoryMapT(..)
, TaxGroup , TaxGroup
, TaxGroupId , TaxGroupId
, TaxGroupT(..) , TaxGroupT(..)
@ -32,6 +35,9 @@ module Jon.Garfield.Types
, Snack , Snack
, SnackId , SnackId
, SnackT(..) , SnackT(..)
, SnackAvailable
, SnackAvailableId
, SnackAvailableT(..)
, Sale , Sale
, SaleId , SaleId
, SaleT(..) , SaleT(..)
@ -43,16 +49,18 @@ module Jon.Garfield.Types
, mkInventoryItemId , mkInventoryItemId
, mkTaxGroupId , mkTaxGroupId
, mkLocationId , mkLocationId
, mkSnackId
, mkSaleId , mkSaleId
, mkSnackId
, mkSnackAvailableId
, mkUserId , mkUserId
, unOverviewId , unOverviewId
, unInventoryItemId , unInventoryItemId
, unInventoryItemGroupId , unInventoryItemGroupId
, unTaxGroupId , unTaxGroupId
, unLocationId , unLocationId
, unSnackId
, unSaleId , unSaleId
, unSnackId
, unSnackAvailableId
, unUserId , unUserId
) where ) where
@ -97,6 +105,8 @@ data GarfieldDb f = GarfieldDb
, snacks :: f (TableEntity SnackT) , snacks :: f (TableEntity SnackT)
, sales :: f (TableEntity SaleT) , sales :: f (TableEntity SaleT)
, users :: f (TableEntity UserT) , users :: f (TableEntity UserT)
, inventoryMap :: f (TableEntity InventoryMapT)
, snacksAvailable :: f (TableEntity SnackAvailableT)
} deriving (Generic, Database be) } deriving (Generic, Database be)
setGarfieldEntityName name = setEntitySchema (Just "garfield") <> setEntityName name setGarfieldEntityName name = setEntitySchema (Just "garfield") <> setEntityName name
@ -168,6 +178,14 @@ garfieldDb = defaultDbSettings `withDbModification`
, alwaysSendBalanceMail = "always_send_balance_mail" , alwaysSendBalanceMail = "always_send_balance_mail"
, sendHistoryMail = "send_history_mail" , sendHistoryMail = "send_history_mail"
} }
, inventoryMap = setGarfieldEntityName "inventory_map" <> modifyTableFields tableModification
{ inventoryId = InventoryItemId "inventory_id"
, snackId = SnackId "snack_id"
}
, snacksAvailable = setGarfieldEntityName "snacks_available" <> modifyTableFields tableModification
{ snackId = SnackId "snack_id"
, available = "snack_available"
}
} }
-- Views -- Views
@ -311,11 +329,11 @@ instance Table CorrectionT where
data SnackT f = Snack data SnackT f = Snack
{ id :: C f Int32 { id :: C f Int32
, name :: C f Text , name :: C f Text
, barcode :: C f Text , barcode :: C (Nullable f) Text
, price :: C f Scientific , price :: C f Scientific
, location :: PrimaryKey LocationT f , location :: PrimaryKey LocationT f
-- , snackModifiedBy :: PrimaryKey UserT f -- , snackModifiedBy :: PrimaryKey UserT f
, timestamp :: C f UTCTime , timestamp :: C (Nullable f) UTCTime
, taxGroup :: PrimaryKey TaxGroupT f , taxGroup :: PrimaryKey TaxGroupT f
} deriving (Beamable, Generic) } deriving (Beamable, Generic)
@ -382,3 +400,40 @@ instance Table UserT where
data PrimaryKey UserT f data PrimaryKey UserT f
= UserId { unUserId :: C f Int32 } deriving (Beamable, Generic) = UserId { unUserId :: C f Int32 } deriving (Beamable, Generic)
primaryKey = UserId . (.id) primaryKey = UserId . (.id)
data InventoryMapT f = InventoryMap
{ inventoryId :: PrimaryKey InventoryItemT f
, snackId :: PrimaryKey SnackT f
} deriving (Beamable, Generic)
type InventoryMap = InventoryMapT Identity
type InventoryMapId = PrimaryKey InventoryMapT Identity
deriving instance Show InventoryMap
deriving instance Show InventoryMapId
instance Table InventoryMapT where
data PrimaryKey InventoryMapT f
= InventoryMapId deriving (Beamable, Generic)
primaryKey _ = InventoryMapId
data SnackAvailableT f = SnackAvailable
{ snackId :: PrimaryKey SnackT f
, available :: C f Bool
} deriving (Beamable, Generic)
type SnackAvailable = SnackAvailableT Identity
type SnackAvailableId = PrimaryKey SnackAvailableT Identity
mkSnackAvailableId :: SnackId -> SnackAvailableId
mkSnackAvailableId = SnackAvailableId
deriving instance Show SnackAvailable
deriving instance Show SnackAvailableId
instance Table SnackAvailableT where
data PrimaryKey SnackAvailableT f
= SnackAvailableId { unSnackAvailableId :: PrimaryKey SnackT f } deriving (Beamable, Generic)
primaryKey = SnackAvailableId . (.snackId)

View File

@ -45,6 +45,8 @@ type JonAPI =
:> Post '[JSON] [OverviewItemDTO] :> Post '[JSON] [OverviewItemDTO]
:<|> "getLocations" :> Summary "Get a list of all locations" :<|> "getLocations" :> Summary "Get a list of all locations"
:> Post '[JSON] [Location] :> Post '[JSON] [Location]
:<|> "adjustInventory" :> ReqBody '[JSON] AdjustInventoryP
:> PostNoContent
:<|> "transferInventory" :> Summary "Transfer inventory between items" :<|> "transferInventory" :> Summary "Transfer inventory between items"
:> Description "If `amount` is negative, its absolute value is transferred in the opposite direction." :> Description "If `amount` is negative, its absolute value is transferred in the opposite direction."
:> ReqBody '[JSON] TransferInventoryP :> ReqBody '[JSON] TransferInventoryP
@ -59,9 +61,15 @@ type SnackAPI =
"createSnack" :> Summary "Create a snack" "createSnack" :> Summary "Create a snack"
:> ReqBody '[JSON] CreateSnackP :> ReqBody '[JSON] CreateSnackP
:> Post '[JSON] SnackId :> Post '[JSON] SnackId
:<|> "getSnacksByItemId" :> Summary "Get active snacks by item id"
:> ReqBody '[JSON] GetSnacksByItemIdP
:> Post '[JSON] [Snack]
:<|> "updateSnack" :> Summary "Update a snack" :<|> "updateSnack" :> Summary "Update a snack"
:> ReqBody '[JSON] UpdateSnackP :> ReqBody '[JSON] UpdateSnackP
:> Post '[JSON] SnackId :> Post '[JSON] SnackId
:<|> "deleteSnack" :> Summary "Delete a snack"
:> ReqBody '[JSON] DeleteSnackP
:> PostNoContent
data GetUnsoundBarcodesP = GetUnsoundBarcodesP data GetUnsoundBarcodesP = GetUnsoundBarcodesP
{ location :: LocationId { location :: LocationId
@ -90,6 +98,12 @@ data GetActiveItemsP = GetActiveItemsP
, location :: LocationId , location :: LocationId
} deriving (Generic, FromJSON, ToSchema) } deriving (Generic, FromJSON, ToSchema)
data AdjustInventoryP = AdjustInventoryP
{ item :: InventoryItemId
, amount :: Int64
, description :: Text
} deriving (Generic, FromJSON, ToSchema)
data TransferInventoryP = TransferInventoryP data TransferInventoryP = TransferInventoryP
{ transfers :: [InventoryTransferDTO] { transfers :: [InventoryTransferDTO]
} deriving (Generic, FromJSON, ToSchema) } deriving (Generic, FromJSON, ToSchema)
@ -112,6 +126,10 @@ data CreateSnackP = CreateSnackP
, location :: LocationId , location :: LocationId
} deriving (Generic, FromJSON, ToSchema) } deriving (Generic, FromJSON, ToSchema)
data GetSnacksByItemIdP = GetSnacksByItemIdP
{ item :: InventoryItemId
} deriving (Generic, FromJSON, ToSchema)
data UpdateSnackP = UpdateSnackP data UpdateSnackP = UpdateSnackP
{ snack :: SnackId { snack :: SnackId
, name :: Text , name :: Text
@ -120,6 +138,10 @@ data UpdateSnackP = UpdateSnackP
, taxGroup :: TaxGroupId , taxGroup :: TaxGroupId
} deriving (Generic, FromJSON, ToSchema) } deriving (Generic, FromJSON, ToSchema)
data DeleteSnackP = DeleteSnackP
{ snack :: SnackId
} deriving (Generic, FromJSON, ToSchema)
-- Orphan instances for database types -- Orphan instances for database types
-- needed for serialization and swagger doc -- needed for serialization and swagger doc
@ -165,6 +187,9 @@ deriving instance ToSchema InventoryItem
deriving instance ToJSON Location deriving instance ToJSON Location
deriving instance ToSchema Location deriving instance ToSchema Location
deriving instance ToJSON Snack
deriving instance ToSchema Snack
-- server -- server
server :: Connection -> Server JonAPI server :: Connection -> Server JonAPI
@ -173,10 +198,13 @@ server conn =
:<|> getOverviewItems :<|> getOverviewItems
:<|> getActiveItems :<|> getActiveItems
:<|> getLocations :<|> getLocations
:<|> adjustInventory
:<|> transferInventory :<|> transferInventory
:<|> disableItems :<|> disableItems
:<|> createSnack :<|> createSnack
:<|> getSnacksByItemId
:<|> updateSnack :<|> updateSnack
:<|> deleteSnack
where where
getUnsoundBarcodes :: GetUnsoundBarcodesP -> Handler [UnsoundBarcodeDTO] getUnsoundBarcodes :: GetUnsoundBarcodesP -> Handler [UnsoundBarcodeDTO]
getUnsoundBarcodes params = do getUnsoundBarcodes params = do
@ -199,6 +227,10 @@ server conn =
getLocations = do getLocations = do
liftIO $ Queries.runSelect conn Queries.locations liftIO $ Queries.runSelect conn Queries.locations
adjustInventory params = do
liftIO $ Queries.runInserts conn [Queries.adjustInventory params.item params.amount params.description]
pure NoContent
transferInventory :: TransferInventoryP -> Handler NoContent transferInventory :: TransferInventoryP -> Handler NoContent
transferInventory params = do transferInventory params = do
liftIO $ Queries.runInserts conn $ map (\t -> Queries.transfer t.from t.to t.amount) params.transfers liftIO $ Queries.runInserts conn $ map (\t -> Queries.transfer t.from t.to t.amount) params.transfers
@ -217,6 +249,9 @@ server conn =
params.taxGroup params.taxGroup
params.location params.location
getSnacksByItemId params = do
liftIO $ Queries.runSelect conn $ Queries.getSnacksByItemId params.item
updateSnack params = do updateSnack params = do
liftIO $ Queries.runFunction conn $ Queries.snackUpdate liftIO $ Queries.runFunction conn $ Queries.snackUpdate
params.snack params.snack
@ -225,9 +260,14 @@ server conn =
params.price params.price
params.taxGroup params.taxGroup
deleteSnack params = do
liftIO $ Queries.runFunction conn $ Queries.snackDelete params.snack
pure NoContent
jonSwaggerDoc :: Swagger jonSwaggerDoc :: Swagger
jonSwaggerDoc = toSwagger (Proxy :: Proxy JonAPI) jonSwaggerDoc = toSwagger (Proxy :: Proxy JonAPI)
& info . title .~ "jon API" & info . title .~ "jon API"
& info . version .~ "0.1.1" & info . version .~ "0.1.1"
-- & applyTags [Tag "jon RPC" (Just "Crudely improvised Garfield API") Nothing] & applyTags [Tag "jon RPC" (Just "Crudely improvised Garfield API") Nothing]
& applyTagsFor (operationsOf $ toSwagger (Proxy :: Proxy SnackAPI)) [Tag "Snacks" Nothing Nothing] -- Doesn't work :(
-- & applyTagsFor (operationsOf $ toSwagger (Proxy :: Proxy SnackAPI)) [Tag "Snacks" Nothing Nothing]

View File

@ -5,11 +5,10 @@
<link rel="stylesheet" href="./jon.css"> <link rel="stylesheet" href="./jon.css">
</head> </head>
<body> <body>
<div class="jon-elm"></div>
<!-- Compiled by Elm --> <!-- Compiled by Elm -->
<script src="./jon.js"></script> <script src="./jon.js"></script>
<script> <script>
Elm.Main.init({ node: document.querySelector('.jon-elm') }); Elm.Main.init();
</script> </script>
</body> </body>
</html> </html>

23
static/jon.css Normal file
View File

@ -0,0 +1,23 @@
html {
font-size: 16px;
}
button, input {
font-size: 0.8rem;
}
body {
font-family: Arial, Helvetica, sans-serif;
}
table {
border-collapse: collapse;
}
th, td {
padding: .2em .4em;
}
tr:not(:first-child):hover, tbody tr:hover {
background-color: lightblue;
}