diff --git a/public/data/object/examples.json b/public/data/object/examples.json index 9bbf5246f..77665fa48 100644 --- a/public/data/object/examples.json +++ b/public/data/object/examples.json @@ -1,11 +1,11 @@ [ { "id": "0d08a9ff-5683-4d3d-a7b8-dce74f37cd3b", - "name": "Objet par défaut", + "name": "Objet vide", "scope": "object", "category": "", "query": { - "processes": [] + "items": [] } }, { @@ -14,7 +14,7 @@ "scope": "veli", "category": "", "query": { - "processes": [] + "items": [{ "name": "Composant par défaut", "processes": [], "quantity": 0 }] } }, { @@ -23,14 +23,38 @@ "scope": "object", "category": "", "query": { - "processes": [ + "items": [ { - "process_id": "07e9e916-e02b-45e2-a298-2b5084de6242", - "amount": 0.00088 + "name": "Pied 70 cm (plein bois)", + "processes": [ + { + "process_id": "07e9e916-e02b-45e2-a298-2b5084de6242", + "amount": 0.00022 + } + ], + "quantity": 4 }, { - "process_id": "3295b2a5-328a-4c00-b046-e2ddeb0da823", - "amount": 1.645313 + "name": "Dossier plastique (PP)", + "processes": [ + { + "process_id": "3295b2a5-328a-4c00-b046-e2ddeb0da823", + "amount": 0.734063 + } + ], + + "quantity": 1 + }, + { + "name": "Assise plastique (PP)", + "processes": [ + { + "process_id": "3295b2a5-328a-4c00-b046-e2ddeb0da823", + "amount": 0.91125 + } + ], + + "quantity": 1 } ] } @@ -41,10 +65,28 @@ "scope": "object", "category": "", "query": { - "processes": [ + "items": [ + { + "name": "Pied 90 cm (plein bois)", + "processes": [ + { + "process_id": "07e9e916-e02b-45e2-a298-2b5084de6242", + "amount": 0.007065 + } + ], + + "quantity": 4 + }, { - "process_id": "07e9e916-e02b-45e2-a298-2b5084de6242", - "amount": 0.002 + "name": "Plateau 200x100 (chêne)", + "processes": [ + { + "process_id": "07e9e916-e02b-45e2-a298-2b5084de6242", + "amount": 0.14 + } + ], + + "quantity": 1 } ] } @@ -55,10 +97,16 @@ "scope": "veli", "category": "", "query": { - "processes": [ + "items": [ { - "process_id": "3295b2a5-328a-4c00-b046-e2ddeb0da823", - "amount": 35 + "name": "Cadre plastique", + "processes": [ + { + "process_id": "3295b2a5-328a-4c00-b046-e2ddeb0da823", + "amount": 35 + } + ], + "quantity": 1 } ] } diff --git a/src/Data/Object/Query.elm b/src/Data/Object/Query.elm index affa6f507..1f5e3de12 100644 --- a/src/Data/Object/Query.elm +++ b/src/Data/Object/Query.elm @@ -1,36 +1,47 @@ module Data.Object.Query exposing ( Amount - , Item + , Component + , ProcessItem + , Quantity , Query - , amount , amountToFloat , b64encode , buildApiQuery , decode , default - , defaultItem + , defaultComponent , encode , parseBase64Query - , removeItem + , quantity + , quantityToInt + , removeComponent , toString - , updateItem + , updateComponent ) import Base64 import Data.Object.Process as Process exposing (Process) import Data.Scope as Scope exposing (Scope) import Json.Decode as Decode exposing (Decoder) +import Json.Decode.Pipeline as Pipe import Json.Encode as Encode import Result.Extra as RE import Url.Parser as Parser exposing (Parser) type alias Query = - { items : List Item + { components : List Component } -type alias Item = +type alias Component = + { name : String + , processes : List ProcessItem + , quantity : Quantity + } + + +type alias ProcessItem = { amount : Amount , processId : Process.Id } @@ -40,9 +51,8 @@ type Amount = Amount Float -amount : Float -> Amount -amount = - Amount +type Quantity + = Quantity Int amountToFloat : Amount -> Float @@ -50,6 +60,16 @@ amountToFloat (Amount float) = float +quantity : Int -> Quantity +quantity int = + Quantity int + + +quantityToInt : Quantity -> Int +quantityToInt (Quantity int) = + int + + buildApiQuery : Scope -> String -> Query -> String buildApiQuery scope clientUrl query = """curl -sS -X POST %apiUrl% \\ @@ -63,57 +83,77 @@ buildApiQuery scope clientUrl query = decode : Decoder Query decode = - Decode.map Query - (Decode.field "processes" (Decode.list decodeItem)) + Decode.succeed Query + |> Pipe.required "items" (Decode.list decodeItem) -decodeItem : Decoder Item +decodeItem : Decoder Component decodeItem = - Decode.map2 Item + Decode.map3 Component + (Decode.field "name" Decode.string) + (Decode.field "processes" (Decode.list decodeProcessItem)) + (Decode.field "quantity" (Decode.map Quantity Decode.int)) + + +decodeProcessItem : Decoder ProcessItem +decodeProcessItem = + Decode.map2 ProcessItem (Decode.field "amount" (Decode.map Amount Decode.float)) (Decode.field "process_id" Process.decodeId) default : Query default = - { items = [] } + { components = [] } -defaultItem : Process -> Item -defaultItem process = - { amount = Amount 1, processId = process.id } +defaultComponent : Component +defaultComponent = + { name = "Composant par défaut" + , processes = [] + , quantity = Quantity 1 + } encode : Query -> Encode.Value encode query = Encode.object - [ ( "processes" - , Encode.list encodeItem query.items + [ ( "items" + , Encode.list encodeItem query.components ) ] -encodeItem : Item -> Encode.Value +encodeItem : Component -> Encode.Value encodeItem item = Encode.object - [ ( "amount", item.amount |> amountToFloat |> Encode.float ) - , ( "process_id", Process.encodeId item.processId ) + [ ( "name", item.name |> Encode.string ) + , ( "processes", item.processes |> Encode.list encodeProcessItem ) + , ( "quantity", item.quantity |> quantityToInt |> Encode.int ) + ] + + +encodeProcessItem : ProcessItem -> Encode.Value +encodeProcessItem processItem = + Encode.object + [ ( "amount", processItem.amount |> amountToFloat |> Encode.float ) + , ( "process_id", Process.encodeId processItem.processId ) ] -removeItem : Process.Id -> Query -> Query -removeItem processId query = - { query | items = query.items |> List.filter (.processId >> (/=) processId) } +removeComponent : String -> Query -> Query +removeComponent name ({ components } as query) = + { query | components = components |> List.filter (.name >> (/=) name) } -updateItem : Item -> Query -> Query -updateItem newItem query = +updateComponent : Component -> Query -> Query +updateComponent newItem query = { query - | items = - query.items + | components = + query.components |> List.map (\item -> - if item.processId == newItem.processId then + if item.name == newItem.name then newItem else @@ -124,23 +164,32 @@ updateItem newItem query = toString : List Process -> Query -> Result String String toString processes = - .items - >> List.map - (\item -> - item.processId - |> Process.findById processes - |> Result.map - (\process -> - String.fromFloat (amountToFloat item.amount) - ++ process.unit - ++ " " - ++ process.displayName - ) - ) - >> RE.combine + .components + >> RE.combineMap (itemToString processes) >> Result.map (String.join ", ") +itemToString : List Process -> Component -> Result String String +itemToString processes item = + item.processes + |> RE.combineMap (processItemToString processes) + |> Result.map (String.join " | ") + |> Result.map (\processesString -> String.fromInt (quantityToInt item.quantity) ++ " " ++ item.name ++ " [ " ++ processesString ++ " ]") + + +processItemToString : List Process -> ProcessItem -> Result String String +processItemToString processes processItem = + processItem.processId + |> Process.findById processes + |> Result.map + (\process -> + String.fromFloat (amountToFloat processItem.amount) + ++ process.unit + ++ " " + ++ process.displayName + ) + + -- Parser diff --git a/src/Data/Object/Simulator.elm b/src/Data/Object/Simulator.elm index 8bd7d58ed..c3a8e3f49 100644 --- a/src/Data/Object/Simulator.elm +++ b/src/Data/Object/Simulator.elm @@ -1,6 +1,6 @@ module Data.Object.Simulator exposing ( Results(..) - , availableProcesses + , availableComponents , compute , emptyResults , expandItems @@ -13,7 +13,8 @@ module Data.Object.Simulator exposing import Data.Impact as Impact exposing (Impacts, noStepsImpacts) import Data.Impact.Definition as Definition import Data.Object.Process as Process exposing (Process) -import Data.Object.Query as Query exposing (Item, Query) +import Data.Object.Query as Query exposing (Component, ProcessItem, Query, quantityToInt) +import List.Extra as LE import Mass exposing (Mass) import Quantity import Result.Extra as RE @@ -28,47 +29,73 @@ type Results } -availableProcesses : Db -> Query -> List Process -availableProcesses { object } query = + +-- FIX: read the components from a file +-- For now take the components from the example and consider that they are unique by name + + +availableComponents : Db -> Query -> List Component +availableComponents { object } query = let - usedIds = - List.map .processId query.items + -- FIX: For now, consider that components are unique by name, we should + -- replace it with ids later on + usedNames = + query.components + |> List.map .name in - object.processes - |> List.filter (\{ id } -> not (List.member id usedIds)) + object.examples + |> List.concatMap (.query >> .components) + |> LE.uniqueBy .name + |> List.filter (\{ name } -> not (List.member name usedNames)) + |> List.sortBy .name + + +addResults : Results -> Results -> Results +addResults (Results results) (Results acc) = + Results + { acc + | impacts = Impact.sumImpacts [ results.impacts, acc.impacts ] + , items = Results results :: acc.items + , mass = Quantity.sum [ results.mass, acc.mass ] + } compute : Db -> Query -> Result String Results compute db query = - query.items + query.components |> List.map (computeItemResults db) |> RE.combine + |> Result.map (List.foldr addResults emptyResults) + + +computeItemResults : Db -> Component -> Result String Results +computeItemResults db item = + item.processes + |> List.map (computeProcessItemResults db) + |> RE.combine + |> Result.map (List.foldr addResults emptyResults) |> Result.map - (List.foldr - (\(Results { impacts, mass }) (Results acc) -> - Results - { acc - | impacts = Impact.sumImpacts [ impacts, acc.impacts ] - , items = Results { impacts = impacts, items = [], mass = mass } :: acc.items - , mass = Quantity.sum [ mass, acc.mass ] - } - ) - emptyResults + (\(Results { impacts, mass, items }) -> + Results + { impacts = Impact.sumImpacts (List.repeat (quantityToInt item.quantity) impacts) + , items = items + , mass = Quantity.sum (List.repeat (quantityToInt item.quantity) mass) + } ) -computeItemResults : Db -> Item -> Result String Results -computeItemResults { object } { amount, processId } = +computeProcessItemResults : Db -> ProcessItem -> Result String Results +computeProcessItemResults { object } { amount, processId } = processId |> Process.findById object.processes |> Result.map (\process -> - Results - { impacts = + let + impacts = process.impacts |> Impact.mapImpacts (\_ -> Quantity.multiplyBy (Query.amountToFloat amount)) - , items = [] - , mass = + + mass = Mass.kilograms <| if process.unit == "kg" then Query.amountToFloat amount @@ -76,6 +103,11 @@ computeItemResults { object } { amount, processId } = else -- apply density Query.amountToFloat amount * process.density + in + Results + { impacts = impacts + , items = [ Results { impacts = impacts, items = [], mass = mass } ] + , mass = mass } ) @@ -89,14 +121,25 @@ emptyResults = } -expandItems : Db -> Query -> Result String (List ( Query.Amount, Process )) +expandItems : Db -> Query -> Result String (List ( Query.Quantity, String, List ( Query.Amount, Process ) )) expandItems db = - .items - >> List.map (\{ amount, processId } -> ( amount, processId )) - >> List.map (RE.combineMapSecond (Process.findById db.object.processes)) + .components + >> List.map + (\item -> + expandProcesses db item.processes + |> Result.map (\processes -> ( item.quantity, item.name, processes )) + ) >> RE.combine +expandProcesses : Db -> List ProcessItem -> Result String (List ( Query.Amount, Process )) +expandProcesses db processes = + processes + |> List.map (\{ amount, processId } -> ( amount, processId )) + |> List.map (RE.combineMapSecond (Process.findById db.object.processes)) + |> RE.combine + + extractImpacts : Results -> Impacts extractImpacts (Results { impacts }) = impacts diff --git a/src/Page/Object.elm b/src/Page/Object.elm index 7c7f620c8..9e65e9bdc 100644 --- a/src/Page/Object.elm +++ b/src/Page/Object.elm @@ -12,12 +12,13 @@ import Autocomplete exposing (Autocomplete) import Browser.Dom as Dom import Browser.Events import Browser.Navigation as Navigation +import Data.AutocompleteSelector as AutocompleteSelector import Data.Bookmark as Bookmark exposing (Bookmark) import Data.Dataset as Dataset import Data.Example as Example exposing (Example) import Data.Impact.Definition as Definition exposing (Definition) import Data.Key as Key -import Data.Object.Process as Process exposing (Process) +import Data.Object.Process exposing (Process) import Data.Object.Query as Query exposing (Query) import Data.Object.Simulator as Simulator exposing (Results) import Data.Scope as Scope exposing (Scope) @@ -28,6 +29,7 @@ import Html.Attributes exposing (..) import Html.Events exposing (..) import Ports import Route +import Set exposing (Set) import Static.Db exposing (Db) import Task import Time exposing (Posix) @@ -50,6 +52,7 @@ type alias Model = , bookmarkName : String , bookmarkTab : BookmarkView.ActiveTab , comparisonType : ComparatorView.ComparisonType + , detailedComponents : Set String , examples : List (Example Query) , impact : Definition , initialQuery : Query @@ -60,24 +63,27 @@ type alias Model = type Modal - = ComparatorModal + = AddComponentModal (Autocomplete Query.Component) + | ComparatorModal | NoModal | SelectExampleModal (Autocomplete Query) type Msg - = AddItem Query.Item - | CopyToClipBoard String + = CopyToClipBoard String | DeleteBookmark Bookmark | NoOp + | OnAutocompleteAddComponent (Autocomplete.Msg Query.Component) | OnAutocompleteExample (Autocomplete.Msg Query) | OnAutocompleteSelect + | OnAutocompleteSelectComponent | OpenComparator - | RemoveItem Process.Id + | RemoveComponent String | SaveBookmark | SaveBookmarkWithTime String Bookmark.Query Posix | SelectAllBookmarks | SelectNoBookmarks + | SetDetailedComponents (Set String) | SetModal Modal | SwitchBookmarksTab BookmarkView.ActiveTab | SwitchComparisonType ComparatorView.ComparisonType @@ -85,7 +91,7 @@ type Msg | SwitchImpactsTab ImpactTabs.Tab | ToggleComparedSimulation Bookmark Bool | UpdateBookmarkName String - | UpdateItem Query.Item + | UpdateComponent Query.Component init : Scope -> Definition.Trigram -> Maybe Query -> Session -> ( Model, Session, Cmd Msg ) @@ -110,6 +116,7 @@ init scope trigram maybeUrlQuery session = else ComparatorView.Steps + , detailedComponents = Set.empty , examples = examples , impact = Definition.get trigram session.db.definitions , initialQuery = initialQuery @@ -154,6 +161,7 @@ initFromExample session scope uuid = , bookmarkName = exampleQuery |> suggestBookmarkName session examples , bookmarkTab = BookmarkView.SaveTab , comparisonType = ComparatorView.Subscores + , detailedComponents = Set.empty , examples = examples , impact = Definition.get Definition.Ecs session.db.definitions , initialQuery = exampleQuery @@ -220,10 +228,6 @@ update ({ navKey } as session) msg model = |> Session.objectQueryFromScope model.scope in case ( msg, model.modal ) of - ( AddItem item, _ ) -> - update session (SetModal NoModal) model - |> updateQuery { query | items = item :: query.items } - ( CopyToClipBoard shareableLink, _ ) -> ( model, session, Ports.copyToClipboard shareableLink ) @@ -236,6 +240,19 @@ update ({ navKey } as session) msg model = ( NoOp, _ ) -> ( model, session, Cmd.none ) + ( OnAutocompleteAddComponent autocompleteMsg, AddComponentModal autocompleteState ) -> + let + ( newAutocompleteState, autoCompleteCmd ) = + Autocomplete.update autocompleteMsg autocompleteState + in + ( { model | modal = AddComponentModal newAutocompleteState } + , session + , Cmd.map OnAutocompleteAddComponent autoCompleteCmd + ) + + ( OnAutocompleteAddComponent _, _ ) -> + ( model, session, Cmd.none ) + ( OnAutocompleteExample autocompleteMsg, SelectExampleModal autocompleteState ) -> let ( newAutocompleteState, autoCompleteCmd ) = @@ -256,15 +273,22 @@ update ({ navKey } as session) msg model = ( OnAutocompleteSelect, _ ) -> ( model, session, Cmd.none ) + ( OnAutocompleteSelectComponent, AddComponentModal autocompleteState ) -> + ( model, session, Cmd.none ) + |> selectComponent query autocompleteState + + ( OnAutocompleteSelectComponent, _ ) -> + ( model, session, Cmd.none ) + ( OpenComparator, _ ) -> ( { model | modal = ComparatorModal } , session |> Session.checkComparedSimulations , Cmd.none ) - ( RemoveItem processId, _ ) -> + ( RemoveComponent name, _ ) -> ( model, session, Cmd.none ) - |> updateQuery (Query.removeItem processId query) + |> updateQuery (Query.removeComponent name query) ( SaveBookmark, _ ) -> ( model @@ -299,6 +323,18 @@ update ({ navKey } as session) msg model = ( SelectNoBookmarks, _ ) -> ( model, Session.selectNoBookmarks session, Cmd.none ) + ( SetDetailedComponents detailedComponents, _ ) -> + ( { model | detailedComponents = detailedComponents } + , session + , Cmd.none + ) + + ( SetModal (AddComponentModal autocomplete), _ ) -> + ( { model | modal = AddComponentModal autocomplete } + , session + , Ports.addBodyClass "prevent-scrolling" + ) + ( SetModal ComparatorModal, _ ) -> ( { model | modal = ComparatorModal } , session @@ -356,9 +392,9 @@ update ({ navKey } as session) msg model = ( UpdateBookmarkName newName, _ ) -> ( { model | bookmarkName = newName }, session, Cmd.none ) - ( UpdateItem item, _ ) -> + ( UpdateComponent component, _ ) -> ( model, session, Cmd.none ) - |> updateQuery (Query.updateItem item query) + |> updateQuery (Query.updateComponent component query) commandsForNoModal : Modal -> Cmd Msg @@ -386,6 +422,17 @@ selectExample autocompleteState ( model, session, _ ) = |> updateQuery exampleQuery +selectComponent : Query -> Autocomplete Query.Component -> ( Model, Session, Cmd Msg ) -> ( Model, Session, Cmd Msg ) +selectComponent query autocompleteState ( model, session, _ ) = + let + selectedComponent = + Autocomplete.selectedValue autocompleteState + |> Maybe.withDefault Query.defaultComponent + in + update session (SetModal NoModal) model + |> updateQuery { query | components = query.components ++ [ selectedComponent ] } + + simulatorView : Session -> Model -> Html Msg simulatorView session model = div [ class "row" ] @@ -408,7 +455,7 @@ simulatorView session model = ] , session |> Session.objectQueryFromScope model.scope - |> itemListView session.db model.impact model.results + |> componentListView session.db model |> div [ class "card shadow-sm mb-3" ] ] , div [ class "col-lg-4 bg-white" ] @@ -447,38 +494,33 @@ simulatorView session model = ] -addItemButton : Db -> Query -> Html Msg -addItemButton db query = +addComponentButton : Db -> Query -> Html Msg +addComponentButton db query = let - firstAvailableProcess = - query - |> Simulator.availableProcesses db - |> List.head + availableComponents = + Simulator.availableComponents db query + + autocompleteState = + AutocompleteSelector.init .name availableComponents in button [ class "btn btn-outline-primary w-100" , class "d-flex justify-content-center align-items-center" , class "gap-1 w-100" , id "add-new-element" - , disabled <| firstAvailableProcess == Nothing - , onClick <| - case firstAvailableProcess of - Just process -> - AddItem (Query.defaultItem process) - - Nothing -> - NoOp + , disabled <| List.length availableComponents == 0 + , onClick (SetModal (AddComponentModal autocompleteState)) ] [ i [ class "icon icon-plus" ] [] - , text "Ajouter un élément" + , text "Ajouter un composant" ] -itemListView : Db -> Definition -> Results -> Query -> List (Html Msg) -itemListView db selectedImpact results query = +componentListView : Db -> Model -> Query -> List (Html Msg) +componentListView db { detailedComponents, impact, results } query = [ div [ class "card-header d-flex align-items-center justify-content-between" ] [ h2 [ class "h5 mb-0" ] - [ text "Éléments" + [ text "Production des composants" , Link.smallPillExternal -- FIXME: link to Veli explorer? [ Route.href (Route.Explore Scope.Object (Dataset.ObjectProcesses Nothing)) @@ -488,7 +530,7 @@ itemListView db selectedImpact results query = [ Icon.search ] ] ] - , if List.isEmpty query.items then + , if List.isEmpty query.components then div [ class "card-body" ] [ text "Aucun élément." ] else @@ -506,65 +548,96 @@ itemListView db selectedImpact results query = [ table [ class "table mb-0" ] [ thead [] [ tr [ class "fs-7 text-muted" ] - [ th [ class "ps-3", scope "col" ] [ text "Quantité" ] - , th [ scope "col" ] [ text "Procédé" ] - , th [ scope "col" ] [ text "Densité" ] + [ th [] [] + , th [ class "ps-0", scope "col" ] [ text "Quantité" ] + , th [ scope "col", colspan 2 ] [ text "Composant" ] , th [ scope "col" ] [ text "Masse" ] , th [ scope "col" ] [ text "Impact" ] , th [ scope "col" ] [] ] ] , Simulator.extractItems results - |> List.map2 (itemView selectedImpact) items + |> List.map2 (componentView impact detailedComponents) items + |> List.concat |> tbody [] ] ] - , addItemButton db query + , addComponentButton db query ] -itemView : Definition -> ( Query.Amount, Process ) -> Results -> Html Msg -itemView selectedImpact ( amount, process ) itemResults = - tr [] - [ td [ class "ps-3 align-middle" ] - [ div [ class "input-group", style "min-width" "180px" ] - [ input - [ type_ "number" - , class "form-control text-end" - , amount |> Query.amountToFloat |> String.fromFloat |> value - , step <| - case process.unit of - "kg" -> - "0.01" - - "m3" -> - "0.00001" - - _ -> - "1" - , onInput <| - \str -> - case String.toFloat str of - Just float -> - UpdateItem - { amount = Query.amount float - , processId = process.id - } - - Nothing -> - NoOp +componentView : Definition -> Set String -> ( Query.Quantity, String, List ( Query.Amount, Process ) ) -> Results -> List (Html Msg) +componentView selectedImpact detailedComponents ( quantity, name, processes ) itemResults = + let + collapsed = + not <| Set.member name detailedComponents + in + List.concat + [ [ tr [] + [ th [ class "ps-3 align-middle", scope "col" ] + [ button + [ class "btn btn-link text-dark text-decoration-none font-monospace fs-5 p-0 m-0" + , onClick <| + SetDetailedComponents + (if collapsed then + Set.insert name detailedComponents + + else + Set.remove name detailedComponents + ) + ] + [ if collapsed then + text "▶" + + else + text "▼" + ] ] - [] - , span - [ class "input-group-text justify-content-center fs-8" - , style "width" "38px" + , td [ class "ps-0 align-middle" ] + [ quantity |> quantityInput processes name ] + , td [ class "align-middle text-truncate w-100 fw-bold", colspan 2 ] + [ text name ] + , td [ class "text-end align-middle text-nowrap" ] + [ Format.kg <| Simulator.extractMass itemResults ] + , td [ class "text-end align-middle text-nowrap" ] + [ Simulator.extractImpacts itemResults |> Format.formatImpact selectedImpact ] + , td [ class "pe-3 align-middle text-nowrap" ] + [ button [ class "btn btn-outline-secondary", onClick (RemoveComponent name) ] + [ Icon.trash ] ] - [ text process.unit ] ] - ] + , if not collapsed then + tr [ class "fs-7 text-muted" ] + [ th [] [] + , th [ class "text-end", scope "col" ] [ text "Quantité" ] + , th [ scope "col" ] [ text "Procédé" ] + , th [ scope "col" ] [ text "Densité" ] + , th [ scope "col" ] [ text "Masse" ] + , th [ scope "col" ] [ text "Impact" ] + , th [ scope "col" ] [ text "" ] + ] + + else + text "" + ] + , if not collapsed then + Simulator.extractItems itemResults + |> List.map2 (processView selectedImpact) processes + + else + [] + ] + + +processView : Definition -> ( Query.Amount, Process ) -> Results -> Html Msg +processView selectedImpact ( amount, process ) itemResults = + tr [ class "fs-7" ] + [ td [] [] + , td [ class "text-end text-nowrap" ] + [ Format.amount process amount ] , td [ class "align-middle text-truncate w-100" ] [ text process.displayName ] - , td [ class "align-middle text-end" ] + , td [ class "align-middle text-end text-nowrap" ] [ Format.density process ] , td [ class "text-end align-middle text-nowrap" ] [ Format.kg <| Simulator.extractMass itemResults ] @@ -573,9 +646,43 @@ itemView selectedImpact ( amount, process ) itemResults = |> Format.formatImpact selectedImpact ] , td [ class "pe-3 align-middle text-nowrap" ] - [ button [ class "btn btn-outline-secondary", onClick (RemoveItem process.id) ] - [ Icon.trash ] + [] + ] + + +quantityInput : List ( Query.Amount, Process ) -> String -> Query.Quantity -> Html Msg +quantityInput processes name quantity = + div [ class "input-group", style "min-width" "90px", style "max-width" "120px" ] + [ input + [ type_ "number" + , class "form-control text-end" + , quantity |> Query.quantityToInt |> String.fromInt |> value + , step "1" + , Html.Attributes.min "1" + , onInput <| + \str -> + String.toInt str + |> Maybe.andThen + (\int -> + if int > 0 then + Just int + + else + Nothing + ) + |> Maybe.map + (\nonNullInt -> + -- FIX: don't update components based on their name + -- swith to components ids as soon as they are implemented + UpdateComponent + { name = name + , quantity = Query.quantity nonNullInt + , processes = processes |> List.map (\( amount, process ) -> { amount = amount, processId = process.id }) + } + ) + |> Maybe.withDefault NoOp ] + [] ] @@ -585,6 +692,20 @@ view session model = , [ Container.centered [ class "Simulator pb-3" ] [ simulatorView session model , case model.modal of + AddComponentModal autocompleteState -> + AutocompleteSelectorView.view + { autocompleteState = autocompleteState + , closeModal = SetModal NoModal + , footer = [] + , noOp = NoOp + , onAutocomplete = OnAutocompleteAddComponent + , onAutocompleteSelect = OnAutocompleteSelectComponent + , placeholderText = "tapez ici le nom du composant pour le rechercher" + , title = "Sélectionnez un composant" + , toLabel = .name + , toCategory = \_ -> "" + } + ComparatorModal -> ModalView.view { size = ModalView.ExtraLarge diff --git a/src/Views/Format.elm b/src/Views/Format.elm index 00b856a7a..aa387de5c 100644 --- a/src/Views/Format.elm +++ b/src/Views/Format.elm @@ -1,5 +1,6 @@ module Views.Format exposing - ( complement + ( amount + , complement , days , density , formatFloat @@ -28,6 +29,8 @@ module Views.Format exposing import Area exposing (Area) import Data.Impact as Impact exposing (Impacts) import Data.Impact.Definition exposing (Definition) +import Data.Object.Process as ObjectProcess +import Data.Object.Query as ObjectQuery import Data.Split as Split exposing (Split) import Data.Textile.Economics as Economics import Data.Unit as Unit @@ -151,6 +154,28 @@ complement impact = ] +amount : ObjectProcess.Process -> ObjectQuery.Amount -> Html msg +amount { unit } amount_ = + let + floatAmount = + ObjectQuery.amountToFloat amount_ + in + case unit of + "kg" -> + Mass.kilograms floatAmount + |> kg + + "m3" -> + Volume.cubicMeters floatAmount + |> m3 + + _ -> + String.fromFloat floatAmount + ++ " " + ++ unit + |> text + + kg : Mass -> Html msg kg = Mass.inKilograms >> formatRichFloat 3 "kg" diff --git a/tests/Data/Object/SimulatorTest.elm b/tests/Data/Object/SimulatorTest.elm index 99e805788..77792d808 100644 --- a/tests/Data/Object/SimulatorTest.elm +++ b/tests/Data/Object/SimulatorTest.elm @@ -36,7 +36,7 @@ suite = |> Example.findByName "Table" |> Result.andThen (.query >> getEcsImpact db) |> Result.withDefault 0 - |> Expect.within (Expect.Absolute 1) 47 + |> Expect.within (Expect.Absolute 1) 3979 |> asTest "should compute impact for an example table" ] ]