TastewiseAPI = function() { this.tastewiseGateway = $("link#PortalDocRootURL").attr( "href" ) + "/apps/CRM/index.cfm?ajax=tastewiseGateway&mode=direct"; } TastewiseAPI.prototype.constructor = TastewiseAPI; TastewiseAPI.prototype.execute = function( tastewiseOperation, tastewiseParams ) { let instance = this; let d = $.Deferred(); $.ajax( { type : "GET", url : instance.tastewiseGateway, dataType : "json", data : { operation : tastewiseOperation, parameters : JSON.stringify( tastewiseParams ) }, headers : { fseAjax : true } } ).done( function( result ) { if( result.status ) { d.resolve( result.tastewise ) } else { d.reject( result.tastewise ); } }).fail ( function( x ) { console.log( x ); d.reject( x ) }) return d; } OperatorTastewiseMenu = function( data ) { let instance = this; instance.tastewiseId = data.tastewiseId; instance.tastewiseIdReady = $.Deferred(); if( instance.tastewiseId ) { if( instance.tastewiseId.substring( 0, 3 ) == "twc" && data.operatorId ) { let dataSource = Fse.Data.newDataSource( { object : "OPR.operatorUnits", keyField: "operatorId", paginate : false, objectParams : { operatorId : data.operatorId } } ); dataSource.load().done( function( units ) { let unitTastewiseIdFound = false; for( let x = 0; x < units.length; x++ ) { if( units[x].tastewiseId ) { instance.tastewiseId = units[x].tastewiseId; instance.tastewiseIdReady.resolve(); unitTastewiseIdFound = true; break; } } if( !unitTastewiseIdFound ) { // console.log( "TASTEWISE ID NOT FOUND" ); } }) } else { instance.tastewiseIdReady.resolve(); } } } OperatorTastewiseMenu.prototype.constructor = OperatorTastewiseMenu; OperatorTastewiseMenu.prototype.element = function() { if( this.rootElement ) return this.rootElement; let instance = this; instance.rootElement = $("
").addClass( "OperatorTastewiseMenu" ).css( { "width" : "100%", "height" : "100%" }); instance.menuSearchTextBox = null; instance.menuInsightsButton = $("
").dxButton( { text : "Menu Insights", disabled : true, onClick : function() { instance.showMenuInsights(); } }).dxButton( "instance" ); let box = $("
").dxBox( { height : "100%", direction : "col", items : [ { baseSize : 35, template : function() { return $("
").dxToolbar( { items : [ { location : "before", template : function() { instance.menuSearchTextBox = $("").dxTextBox( { placeholder : "menu search", width : 150, showClearButton : true, onValueChanged : function( e ) { instance.refresh(); } }).dxTextBox( "instance" ); return $("
") .append( $("").text( "Menu powered by Tastewise AI" ).css( { "padding-right" : "10px", "padding-left" : "5px" })) .append( instance.menuSearchTextBox.element().css( { "display" : "inline-block" }) ) } }, { location : "before", template : function() { return instance.menuInsightsButton.element(); } } ] }).css( { "padding-bottom" : "5px", "border-bottom" : "1px solid silver" } ) } }, { ratio : 1, template : function() { instance.menuDisplay = $("
").css( { display : "grid", "grid-template-columns" : "repeat( auto-fill, 400px )", "column-gap" : "5px", "row-gap" : "5px" } ); instance.menuDisplay.append( $("
").text( "Loading, please wait..." ) ); instance.scrollView = $("
").append( instance.menuDisplay ).dxScrollView( {} ).dxScrollView( "instance" ); return instance.scrollView.element(); // let scrollView = $("div>"); // scrollView.append( instance.menuDisplay ); // instance.scrollView = scrollView.dxScollView( {} ).dxScrollView( "instance" ); // return instance.menuDisplay; } } ] }) instance.rootElement.append( box ); instance.tastewiseIdReady.done( function() { instance.refresh(); instance.menuInsightsButton.option( { "disabled" : false } ); }) return instance.rootElement; } OperatorTastewiseMenu.prototype.showMenuInsights = function() { let instance = this; let popup = new TastewiseMenuInsightsPopup( { tastewiseId : instance.tastewiseId } ); popup.show(); } OperatorTastewiseMenu.prototype.refresh = function() { instance = this; if( ! instance.tastewiseAPI ) { instance.tastewiseAPI = new TastewiseAPI(); } let menuOperation = `menus`; let menuParams = { restaurant_id : instance.tastewiseId, search : null } if( instance.menuSearchTextBox ) { let search = instance.menuSearchTextBox.option( "value" ); if( search ) { menuParams.search = search; } } instance.tastewiseAPI.execute( "menus", menuParams ) .done( function( menus ) { // many of the items are repeated, so we are going to consolidate them and use the first one, and add an instance count. let dedupedMenuItems = {}; let data = menus; if( ! Array.isArray( menus )) { data = []; } data.forEach( function( menuItem ) { let menuItemName = menuItem.name.toUpperCase(); if( dedupedMenuItems[menuItemName] ) { dedupedMenuItems[menuItemName].instanceCount++; } else { dedupedMenuItems[menuItemName] = menuItem; dedupedMenuItems[menuItemName].instanceCount = 1; } }) data = []; for( mi in dedupedMenuItems ) { data.push( dedupedMenuItems[mi] ) } instance._updateDisplay( data ); }).fail( function( error ) { console.log( "ERROR" ); console.log( error ); instance._updateDisplay( null ); }) } OperatorTastewiseMenu.prototype._updateDisplay = function( data ) { let instance = this; // if( data ) { // instance.menuDisplay.option( "dataSource", { // store : { // type : "array", // data : data // } // }); // } else { // instance.menuDisplay.option( "dataSource", null ); // } // instance.menuDisplay.repaint(); instance.menuDisplay.empty(); if( ! data ) { instance.menuDisplay.append( $("
").text( "Menu not available" ) ); instance.scrollView.update(); return; } data.forEach( function( itemData ) { itemData.nameSort = null; if( itemData.name ) itemData.nameSort = itemData.name.toUpperCase(); }) let sortedData = DevExpress.data.query(data) .sortBy("nameSort") .toArray(); sortedData.forEach( function( itemData ) { let item = $("
" ).css( { "grid-row" : "span 2", "border-left" : "1px solid #BFC3C5", "border-top" : "1px solid #BFC3C5", "border-bottom": "1px solid #BFC3C5","padding" : "5px", "padding-right": "2px" }) item.append( $("
").text( itemData.name ).css( { "font-size" : "14px", "font-weight": "bold", "margin-bottom" : "4px", "text-transform" : "capitalize", "white-space" : "nowrap", "overflow" : "hidden", "text-overflow" : "ellipsis" } ).attr( "title", itemData.name ) ); let cssHeight = "60px"; if( itemData.instanceCount > 1 ) { cssHeight = "48px"; }; item.append( $("
").css( { "font-size" : "12px", "height": cssHeight, "overflow" : "hidden", "text-overflow" : "ellipsis" } ).text( itemData.description ).attr( "title", itemData.description ) ); if( itemData.instanceCount > 1 ) { item.append( $("
").css( { "font-size" : "10px" } ).text( `appears ${itemData.instanceCount} times`) ); } let price = $("
").css( { "display" : "flex", "align-items" : "center", "justify-content" : "center", "padding" : "5px", "padding-left": "3px", "border-left": "1px solid #BFC3C5", "border-top" : "1px solid #BFC3C5", "border-right" : "1px solid #BFC3C5" }); price.append( $("
" ).text( DevExpress.localization.formatNumber( itemData.price, "currency" ) )); let platform = $("
").css( { "display" : "flex", "align-items" : "center", "justify-content" : "center", "padding" : "5px", "padding-left": "8px", "padding-right" : "10px", "border-left": "1px solid #BFC3C5", "border-top" : "1px solid #BFC3C5", "border-bottom" : "1px solid #BFC3C5", "border-right" : "1px solid #BFC3C5" }); let platformImage = null; if( itemData.platform == "doordash" ) { platformImage = "DoorDash_Logo_gray.svg"; } else if ( itemData.platform == "grubhub" ) { platformImage = "GrubHub_gray.svg"; } else if ( itemData.platform == "yelp" ) { platformImage = "Yelp_Logo_gray.svg"; } if( platformImage ) { let img = $("").attr( { "src" : `${Fse.Portal.appConfiguration.STP.resourcePath}/graphics/${platformImage}` }).css( { "color" : "#BFC3C5", width : "75%" }); // was 100% and was asked to reduce it by 25% platform.append( img ); } else { platform.append( $("
").text( itemData.platform )); } let menuItemElement = $("
").css( { "display" : "grid", "grid-template-columns" : "70% 30%", "width" : "400px", "xborder": "1px solid silver", "padding" : "5px" } ) menuItemElement.append( item ); menuItemElement.append( price ); menuItemElement.append( platform ); instance.menuDisplay.append( menuItemElement ); }) instance.scrollView.update(); } TastewiseMenuInsightsPopup = function( options ) { this.tastewiseId = options.tastewiseId; } TastewiseMenuInsightsPopup.prototype.constructor = TastewiseMenuInsightsPopup; TastewiseMenuInsightsPopup.prototype.getMenuInsights = function() { let instance = this; if( ! instance.tastewiseAPI ) instance.tastewiseAPI = new TastewiseAPI(); let params = { "tw_id" : instance.tastewiseId, "market" : "USA", "customer_id" : `${Fse.Portal.appConfiguration.STP.ownerType}:${Fse.Portal.appConfiguration.STP.ownerId}:-1` } let sku = instance.skuSelect.option( "value" ); let category = instance.categoryTextBox.option( "value" ); if( sku ) { params["sku"] = sku; } else if ( category ) { params["category"] = category; } else { instance.menuInsights = null; instance.tabPanel.option( "selectedIndex", 0 ); instance.tabPanel.repaint(); return; } console.log( params ); let loadPanel = $("
").dxLoadPanel( { message : "Loading Menu Insights..." , onHidden : function( e ) { e.component.element().remove(); e.component.dispose(); } }).appendTo( $("body") ).dxLoadPanel("instance"); loadPanel.show(); instance.tastewiseAPI.execute( "menu_insights", params ).done( function( r ) { console.log( r ); loadPanel.hide(); instance.menuInsights = r; instance.tabPanel.option( "selectedIndex", 0 ); instance.tabPanel.repaint(); }).fail( function() { loadPanel.hide(); instance.menuInsights = null; instance.tabPanel.option( "selectedIndex", 0 ); instance.tabPanel.repaint(); }) } TastewiseMenuInsightsPopup.prototype.executiveSummaryTemplate = function () { let instance = this; let menuInsights = instance.menuInsights; if( ! menuInsights || ! menuInsights.executive_summary ) return $("
" ); let executiveSummary = []; let parsed = menuInsights.executive_summary.split( "\n" ); console.log( parsed ); parsed.forEach( function( line ) { if( line == "" ) return; let parts = line.split( /\*\*/ ); console.log( "parts", parts ); let summaryItem = null; parts.forEach( function( part ) { let cleanPart = part.replace( /^[\W\s]*|\s+$/g, "" ); if( cleanPart == "" ) return; if( summaryItem ) { if( summaryItem.actionFound ) { summaryItem.recommendedAction = cleanPart; executiveSummary.push( summaryItem ); summaryItem = null; return; } if( cleanPart == "Action:" ) { summaryItem.actionFound = 1; return; } summaryItem.detail = cleanPart; } else { summaryItem = { heading : cleanPart } } console.log( "part", part ); console.log( "cleaned", cleanPart ); }) }) console.log( "Summary Parsed", executiveSummary ); return $("
").dxList( { items : executiveSummary, height : "100%", itemTemplate : function( item ) { let container = $("
"); container.append( $("

").text( item.heading )) container.append( $("

").text( item.detail )); container.append( $("

").append( $("").text( "Action:" ) ).append( $("").text( item.recommendedAction ) )); return container; } }).addClass( "executive-summary" ); } TastewiseMenuInsightsPopup.prototype._createTastewiseOpportunityGrid = function( tastewiseOpportunities ) { return $("

").dxDataGrid( { dataSource : tastewiseOpportunities, rowAlternationEnabled : true, scrolling : { mode : "virtual" }, height : "100%", columns : [ { dataField : "image_url", cellTemplate : function( container, options ) { let img = $("").attr( { src : options.data.image_url, width : "100%" }); container.append( img ); }}, // "restaurant_name", "name", "description", "ingredients", "price" // "tw_id", ] }).dxDataGrid( "instance" ); } TastewiseMenuInsightsPopup.prototype.bestSellingOpsTemplate = function () { let instance = this; let menuInsights = instance.menuInsights; if( ! menuInsights || ! menuInsights.bestselling_opportunities ) return $("
" ); return instance._createTastewiseOpportunityGrid( menuInsights.bestselling_opportunities ).element(); } TastewiseMenuInsightsPopup.prototype.ltoOpsTemplate = function () { let instance = this; let menuInsights = instance.menuInsights; if( ! menuInsights || ! menuInsights.lto_opportunities ) return $("
" ); return instance._createTastewiseOpportunityGrid( menuInsights.lto_opportunities ).element(); } TastewiseMenuInsightsPopup.prototype.ingredientComparisonTemplate = function () { let instance = this; let menuInsights = instance.menuInsights; if( ! menuInsights || ! menuInsights.ingredient_comparison ) return $("
" ); return $("
").dxDataGrid( { height : "100%", scrolling : { mode : "virtual" }, dataSource : { sort : [{ getter : "market_share", desc : true }], store : { type : "array", data : menuInsights.ingredient_comparison, key : "name" } }, columns : [ { dataField : "name", caption : "Ingredient" }, { dataField : "market_share", format : { type : "percent", precision : 1 }, dataType : "number", calculateCellValue : function( rowData ) { if( rowData.market_share ) { return rowData.market_share / 100.0; } else { return null; } } }, { dataField : "market_yoy", caption : "Market YoY", dataType : "number", format : { type : "percent", precision : 1 }, calculateCellValue : function( rowData ) { if( rowData.market_yoy ) { return rowData.market_yoy / 100.0; } else { return null; } } }, { dataField : "market_index", dataType : "number", format : { type : "fixedPoint", precision : 0 }}, { dataField : "restaurant_share", caption : "Rest. Share", dataType : "number", format : { type : "percent", precision : 1 }, calculateCellValue : function( rowData ) { if( rowData.restaurant_share ) { return rowData.restaurant_share / 100.0; } else { return null; } } }, { dataField : "restaurant_yoy", caption : "Rest. YoY", dataType : "number", format : { type : "percent", precision : 1 }, calculateCellValue : function( rowData ) { if( rowData.restaurant_yoy ) { return rowData.restaurant_yoy / 100.0; } else { return null; } } }, { dataField : "restaurant_index", caption : "Rest. Index", dataType : "number", format : { type : "fixedPoint", precision : 0 }}, { dataField : "index_signal" } ], rowAlternationEnabled : true, scrolling : { mode : "virtual" } }) } TastewiseMenuInsightsPopup.prototype.dishComparisonTemplate = function () { let instance = this; let menuInsights = instance.menuInsights; if( ! menuInsights || ! menuInsights.dish_comparison ) return $("
" ); return $("
").dxDataGrid( { height : "100%", scrolling : { mode : "virtual" }, dataSource : { sort : [{ getter : "market_share", desc : true }], store : { type : "array", data : menuInsights.dish_comparison, key : "name" } }, columns : [ { dataField : "name", caption : "Dish" }, { dataField : "market_share", format : { type : "percent", precision : 1 }, dataType : "number", calculateCellValue : function( rowData ) { if( rowData.market_share ) { return rowData.market_share / 100.0; } else { return null; } } }, { dataField : "market_yoy", caption : "Market YoY", dataType : "number", format : { type : "percent", precision : 1 }, calculateCellValue : function( rowData ) { if( rowData.market_yoy ) { return rowData.market_yoy / 100.0; } else { return null; } } }, { dataField : "market_index", dataType : "number", format : { type : "fixedPoint", precision : 0 }}, { dataField : "restaurant_share", caption : "Rest. Share", dataType : "number", format : { type : "percent", precision : 1 }, calculateCellValue : function( rowData ) { if( rowData.restaurant_share ) { return rowData.restaurant_share / 100.0; } else { return null; } } }, { dataField : "restaurant_yoy", caption : "Rest. YoY", dataType : "number", format : { type : "percent", precision : 1 }, calculateCellValue : function( rowData ) { if( rowData.restaurant_yoy ) { return rowData.restaurant_yoy / 100.0; } else { return null; } } }, { dataField : "restaurant_index", caption : "Rest. Index", dataType : "number", format : { type : "fixedPoint", precision : 0 }}, { dataField : "index_signal" } ], rowAlternationEnabled : true, scrolling : { mode : "virtual" } }) } TastewiseMenuInsightsPopup.prototype.show = function() { let instance = this; instance.skuSelect = $("
").dxSelectBox( { width : 300, placeholder : "Select a SKU", dataSource : Fse.Data.newDataSource( { object : "CRM.tastewiseSKUs", paginate : true, pageSize : 50, key : "sku" } ), displayExpr : "skuFull", valueExpr : "sku", showClearButton : true, searchEnabled : true, searchExpr : "skuFull", searchMode : "contains", onSelectionChanged : function( e ) { instance.getMenuInsights(); } }).dxSelectBox( "instance" ) instance.categoryTextBox = $("
").dxTextBox( { showClearButton : true, width : 200, placeholder : "Enter a category, keyword, or ingredient", onValueChanged : function( e ) { instance.getMenuInsights(); } }).dxTextBox( "instance" ); let popup = $("
").dxPopup( { title : "Menu Insights powered by Tastewise", width : "80vw", height : "80vh", contentTemplate : function( ){ let box = $("
").dxBox( { height : "100%", direction : "col", items : [ { baseSize : 30, template : function() { return $("
").dxToolbar( { items : [ { location : "before", template : function() { return instance.skuSelect.element(); } }, { location : "before", template : function() { return instance.categoryTextBox.element(); } } ] }) } }, { ratio : 1, template : function() { instance.tabPanel = $("
").dxTabPanel( { height : "100%", items : [ { title : "Summary", template : function() { return $("
").css( { "height" : "100%" }).append( instance.executiveSummaryTemplate() ) } }, { title : "Best Selling", template : function() { return $("
").css( { "height" : "100%" }).append( instance.bestSellingOpsTemplate() ) } }, { title : "LTO", template : function() { return $("
").css( { "height" : "100%" }).append( instance.ltoOpsTemplate() ) } }, { title : "Dish Comparison", template : function() { return $("
").css( { "height" : "100%" }).append( instance.dishComparisonTemplate() ) } }, { title : "Ingredient Comparison", template : function() { return $("
").css( { "height" : "100%" }).append( instance.ingredientComparisonTemplate() ) } }, ] }).dxTabPanel( "instance" ); return instance.tabPanel.element(); } } ] }) return box; } }).appendTo( $("body") ).dxPopup( "show" ) }