diff --git a/core/Date.php b/core/Date.php index 3fb96665e0fddef808f0eb613432f1070cac5ada..22580bd492a0d7a7ca8f8e80a6706f57194ede5e 100644 --- a/core/Date.php +++ b/core/Date.php @@ -19,6 +19,8 @@ use Exception; */ class Date { + const NUM_SECONDS_IN_DAY = 86400; + /** * The stored timestamp is always UTC based. * The returned timestamp via getTimestamp() will have the conversion applied @@ -641,4 +643,15 @@ class Date { return $this->addPeriod(-$n, $period); } + + /** + * Returns the number of days represented by a number of seconds. + * + * @param int $secs + * @return float + */ + public static function secondsToDays($secs) + { + return $secs / self::NUM_SECONDS_IN_DAY; + } } diff --git a/plugins/SegmentEditor/images/down_arrow.png b/libs/jquery/images/down_arrow.png similarity index 100% rename from plugins/SegmentEditor/images/down_arrow.png rename to libs/jquery/images/down_arrow.png diff --git a/plugins/SegmentEditor/images/scroller.png b/libs/jquery/images/scroller.png similarity index 100% rename from plugins/SegmentEditor/images/scroller.png rename to libs/jquery/images/scroller.png diff --git a/plugins/SegmentEditor/images/slide.png b/libs/jquery/images/slide.png similarity index 100% rename from plugins/SegmentEditor/images/slide.png rename to libs/jquery/images/slide.png diff --git a/plugins/SegmentEditor/images/up_arrow.png b/libs/jquery/images/up_arrow.png similarity index 100% rename from plugins/SegmentEditor/images/up_arrow.png rename to libs/jquery/images/up_arrow.png diff --git a/plugins/SegmentEditor/javascripts/jquery.jscrollpane.js b/libs/jquery/jquery.jscrollpane.js similarity index 100% rename from plugins/SegmentEditor/javascripts/jquery.jscrollpane.js rename to libs/jquery/jquery.jscrollpane.js diff --git a/plugins/SegmentEditor/javascripts/jquery.mousewheel.js b/libs/jquery/jquery.mousewheel.js similarity index 100% rename from plugins/SegmentEditor/javascripts/jquery.mousewheel.js rename to libs/jquery/jquery.mousewheel.js diff --git a/plugins/SegmentEditor/javascripts/mwheelIntent.js b/libs/jquery/mwheelIntent.js similarity index 100% rename from plugins/SegmentEditor/javascripts/mwheelIntent.js rename to libs/jquery/mwheelIntent.js diff --git a/plugins/SegmentEditor/stylesheets/jquery.jscrollpane.css b/libs/jquery/stylesheets/jquery.jscrollpane.css similarity index 100% rename from plugins/SegmentEditor/stylesheets/jquery.jscrollpane.css rename to libs/jquery/stylesheets/jquery.jscrollpane.css diff --git a/plugins/SegmentEditor/stylesheets/scroll.less b/libs/jquery/stylesheets/scroll.less similarity index 83% rename from plugins/SegmentEditor/stylesheets/scroll.less rename to libs/jquery/stylesheets/scroll.less index 23c0402e15d98c55488068df25be22d140259ce6..508adb4c7abffdac6b09ce9a3ebd238b37435f7f 100644 --- a/plugins/SegmentEditor/stylesheets/scroll.less +++ b/libs/jquery/stylesheets/scroll.less @@ -54,7 +54,7 @@ .jspTrack { - background: url("plugins/SegmentEditor/images/slide.png") transparent no-repeat 7px; + background: url("../images/slide.png") transparent no-repeat 7px; position: relative; background-size: 20% 100%; /*height: 447px!important;*/ @@ -62,7 +62,7 @@ .jspDrag { - background: url("plugins/SegmentEditor/images/scroller.png") transparent no-repeat; + background: url("../images/scroller.png") transparent no-repeat; background-size: 100% 100%; width: 17px; position: relative; @@ -86,10 +86,10 @@ cursor: pointer; } .jspArrowDown{ - background: url("plugins/SegmentEditor/images/down_arrow.png") transparent no-repeat; + background: url("../images/down_arrow.png") transparent no-repeat; } .jspArrowUp{ - background: url("plugins/SegmentEditor/images/up_arrow.png") transparent no-repeat; + background: url("../images/up_arrow.png") transparent no-repeat; } .jspVerticalBar .jspArrow diff --git a/plugins/CoreHome/CoreHome.php b/plugins/CoreHome/CoreHome.php index c78d5b3d6e148958507c2bdda069bfaed1da86f7..c7b6e01a5f408188e405a274ecbaaaa9120fa377 100644 --- a/plugins/CoreHome/CoreHome.php +++ b/plugins/CoreHome/CoreHome.php @@ -41,6 +41,8 @@ class Piwik_CoreHome extends Plugin public function getCssFiles(&$cssFiles) { $cssFiles[] = "libs/jquery/themes/base/jquery-ui.css"; + $cssFiles[] = "libs/jquery/stylesheets/jquery.jscrollpane.css"; + $cssFiles[] = "libs/jquery/stylesheets/scroll.less"; $cssFiles[] = "plugins/Zeitgeist/stylesheets/base.less"; $cssFiles[] = "plugins/CoreHome/stylesheets/coreHome.less"; $cssFiles[] = "plugins/CoreHome/stylesheets/menu.less"; @@ -62,6 +64,9 @@ class Piwik_CoreHome extends Plugin $jsFiles[] = "libs/jquery/jquery.truncate.js"; $jsFiles[] = "libs/jquery/jquery.scrollTo.js"; $jsFiles[] = "libs/jquery/jquery.history.js"; + $jsFiles[] = "libs/jquery/jquery.jscrollpane.js"; + $jsFiles[] = "libs/jquery/jquery.mousewheel.js"; + $jsFiles[] = "libs/jquery/mwheelIntent.js"; $jsFiles[] = "libs/javascript/sprintf.js"; $jsFiles[] = "plugins/Zeitgeist/javascripts/piwikHelper.js"; $jsFiles[] = "plugins/Zeitgeist/javascripts/ajaxHelper.js"; diff --git a/plugins/CoreHome/javascripts/popover.js b/plugins/CoreHome/javascripts/popover.js index 0ffe6d76196ad6ad7b380f6640583a4281dcf25c..27c07dc33479493481be61199d36f2dfc024e1b3 100644 --- a/plugins/CoreHome/javascripts/popover.js +++ b/plugins/CoreHome/javascripts/popover.js @@ -17,10 +17,11 @@ var Piwik_Popover = (function () { } }; - var openPopover = function (title) { + var openPopover = function (title, dialogClass) { createContainer(); - container.dialog({ + var options = + { title: title, modal: true, width: '950px', @@ -28,6 +29,10 @@ var Piwik_Popover = (function () { resizable: false, autoOpen: true, open: function (event, ui) { + if (dialogClass) { + $(this).parent().addClass(dialogClass).attr('style', ''); + } + $('.ui-widget-overlay').on('click.popover', function () { container.dialog('close'); }); @@ -45,7 +50,9 @@ var Piwik_Popover = (function () { closeCallback = false; } } - }); + }; + + container.dialog(options); // override the undocumented _title function to ensure that the title attribute is not escaped (according to jQueryUI bug #6016) container.data( "uiDialog" )._title = function(title) { @@ -70,7 +77,7 @@ var Piwik_Popover = (function () { * @param {string} [popoverSubject] subject of the popover (e.g. url, optional) * @param {int} [height] height of the popover in px (optional) */ - showLoading: function (popoverName, popoverSubject, height) { + showLoading: function (popoverName, popoverSubject, height, dialogClass) { var loading = $(document.createElement('div')).addClass('Piwik_Popover_Loading'); var loadingMessage = popoverSubject ? translations.General_LoadingPopoverFor_js : @@ -94,7 +101,7 @@ var Piwik_Popover = (function () { } if (!isOpen) { - openPopover(); + openPopover(null, dialogClass); } this.setContent(loading); @@ -203,9 +210,18 @@ var Piwik_Popover = (function () { * @param {string} url * @param {string} loadingName */ - createPopupAndLoadUrl: function (url, loadingName) { + createPopupAndLoadUrl: function (url, loadingName, dialogClass) { + // make sure the minimum top position of the popover is 106px + var ensureMinimumTop = function () { + var popoverContainer = $('#Piwik_Popover').parent(); + if (popoverContainer.position().top < 106) { + popoverContainer.css('top', '106px'); + } + }; + // open the popover - var box = Piwik_Popover.showLoading(loadingName); + var box = Piwik_Popover.showLoading(loadingName, null, null, dialogClass); + ensureMinimumTop(); var callback = function (html) { function setPopoverTitleIfOneFoundInContainer() { @@ -218,6 +234,7 @@ var Piwik_Popover = (function () { Piwik_Popover.setContent(html); setPopoverTitleIfOneFoundInContainer(); + ensureMinimumTop(); }; var ajaxRequest = new ajaxHelper(); ajaxRequest.addParams(piwikHelper.getArrayFromQueryString(url), 'get'); @@ -226,6 +243,4 @@ var Piwik_Popover = (function () { ajaxRequest.send(false); } }; - -})(); - +})(); \ No newline at end of file diff --git a/plugins/Live/API.php b/plugins/Live/API.php index 3354c67335d85b5bb06b64d14207e76b3b6ac2d6..739973bb99a05006c1a8f794671c5b0ca41caf56 100644 --- a/plugins/Live/API.php +++ b/plugins/Live/API.php @@ -155,6 +155,158 @@ class Piwik_Live_API return $dataTable; } + const VISITOR_PROFILE_MAX_VISITS_TO_AGGREGATE = 100; + const VISITOR_PROFILE_MAX_VISITS_TO_SHOW = 10; + const VISITOR_PROFILE_DATE_FORMAT = '%day% %shortMonth% %longYear%'; + + /** + * TODO + * TODO: add abandoned cart info. + * TODO: check for most recent vs. first visit + * TODO: make sure ecommerce is enabled for site, check for goals plugin, etc. + */ + public function getVisitorProfile($idSite, $period, $date, $idVisitor, $segment = false) + { + if ($segment !== false) { + $segment .= '&'; + } + $segment .= 'visitorId==' . $idVisitor; // TODO what happens when visitorId is in the segment? + + $visits = $this->getLastVisitsDetails($idSite, $period, $date, $segment, $filter_limit = self::VISITOR_PROFILE_MAX_VISITS_TO_AGGREGATE); + if ($visits->getRowsCount() == 0) { + return array(); + } + + $result = array(); + + // use the most recent visit for IP/browser/OS/etc. info + // TODO: could just do all of this in twig/JS... really need to do it here? + $mostRecentVisit = $visits->getFirstRow(); + $result['latestVisitIp'] = $mostRecentVisit->getColumn('visitIp'); + $result['visitorId'] = $mostRecentVisit->getColumn('visitorId'); + $result['browserCode'] = $mostRecentVisit->getColumn('browserCode'); + $result['browserName'] = Piwik_UserSettings_getBrowserFromBrowserVersion($mostRecentVisit->getColumn('browserName')); + $result['browserLogo'] = $mostRecentVisit->getColumn('browserIcon'); + $result['operatingSystemCode'] = $mostRecentVisit->getColumn('operatingSystemCode'); + $result['operatingSystemShortName'] = $mostRecentVisit->getColumn('operatingSystemShortName'); + $result['operatingSystemLogo'] = $mostRecentVisit->getColumn('operatingSystemIcon'); + $result['resolution'] = $mostRecentVisit->getColumn('resolution'); + $result['customVariables'] = $mostRecentVisit->getColumn('customVariables'); + + // aggregate all requested visits info for total_* info + $result['totalVisits'] = 0; + $result['totalVisitDuration'] = 0; + $result['totalActionCount'] = 0; + $result['totalGoalConversions'] = 0; + $result['totalEcommerceConversions'] = 0; + $result['totalEcommerceRevenue'] = 0; + $result['totalEcommerceItems'] = 0; + $result['totalAbandonedCarts'] = 0; + $result['totalAbandonedCartsRevenue'] = 0; + $result['totalAbandonedCartsItems'] = 0; + foreach ($visits->getRows() as $visit) { + ++$result['totalVisits']; + + $result['totalVisitDuration'] += $visit->getColumn('visitDuration'); + $result['totalActionCount'] += $visit->getColumn('actions'); + $result['totalGoalConversions'] += $visit->getColumn('goalConversions'); + + // individual goal conversions are stored in action details + foreach ($visit->getColumn('actionDetails') as $action) { + if ($action['type'] == 'goal') { // handle goal conversion + $idGoal = $action['goalId']; + + if (!isset($result['totalConversionsByGoal'][$idGoal])) { + $result['totalConversionsByGoal'][$idGoal] = 0; + } + ++$result['totalConversionsByGoal'][$idGoal]; + + if (!empty($action['revenue'])) { + if (!isset($result['totalRevenueByGoal'][$idGoal])) { + $result['totalRevenueByGoal'][$idGoal] = 0; + } + $result['totalRevenueByGoal'][$idGoal] += $action['revenue']; + } + } else if ($action['type'] == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER) { // handle ecommerce order + ++$result['totalEcommerceConversions']; + $result['totalEcommerceRevenue'] += $action['revenue']; + $result['totalEcommerceItems'] += $action['items']; + } else if ($action['type'] == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART) { // handler abandoned cart + ++$result['totalAbandonedCarts']; + $result['totalAbandonedCartsRevenue'] += $action['revenue']; + $result['totalAbandonedCartsItems'] += $action['items']; + } + } + } + + $result['totalVisitDurationPretty'] = Piwik::getPrettyTimeFromSeconds($result['totalVisitDuration']); + + // use requested visits for first/last visit info + $result['firstVisit'] = $this->getVisitorProfileVisitSummary(end($visits->getRows())); + $result['lastVisit'] = $this->getVisitorProfileVisitSummary(reset($visits->getRows())); + + // use N most recent visits for last_visits + $visits->deleteRowsOffset(self::VISITOR_PROFILE_MAX_VISITS_TO_SHOW); + $result['lastVisits'] = $visits; + + // use the right date format for the pretty server date + $timezone = Site::getTimezoneFor($idSite); + foreach ($result['lastVisits']->getRows() as $visit) { + $dateTimeVisitFirstAction = Date::factory($visit->getColumn('firstActionTimestamp'), $timezone); + $dateTimePretty = $dateTimeVisitFirstAction->getLocalized(self::VISITOR_PROFILE_DATE_FORMAT); + + $visit->setColumn('serverDatePrettyFirstAction', $dateTimePretty); + } + + return $result; + } + + /** + * Returns a summary for an important visit. Used to describe the first & last visits of a visitor. + * + * @param Piwik\DataTable\Row $visit + */ + private function getVisitorProfileVisitSummary($visit) + { + $today = Date::today(); + + $serverDate = $visit->getColumn('serverDate'); + return array( + 'date' => $serverDate, + 'prettyDate' => Date::factory($serverDate)->getLocalized(self::VISITOR_PROFILE_DATE_FORMAT), + 'daysAgo' => (int)Date::secondsToDays($today->getTimestamp() - Date::factory($serverDate)->getTimestamp()), + 'referralSummary' => $this->getReferrerSummaryForVisit($visit), + ); + } + + /** + * Returns a summary for a visit's referral. + * + * @param Piwik\DataTable\Row $visit + */ + private function getReferrerSummaryForVisit($visit) + { + $referrerType = $visit->getColumn('referrerType'); + if ($referrerType === false + || $referrerType == 'direct' + ) { + $result = Piwik_Translate('Referers_DirectEntry'); + } else if ($referrerType == 'search') { + $result = $visit->getColumn('referrerName'); + + $keyword = $visit->getColumn('referrerKeyword'); + if ($keyword !== false) { + $result .= ' (' . $keyword . ')'; + } + } else if ($referrerType == 'campaign') { + $result = Piwik_Translate('Referers_ColumnCampaign') . ' (' . $visit->getColumn('referrerName') . ')'; + } else { + $result = $visit->getColumn('referrerName'); + } + + return $result; + } + /** * @deprecated */ diff --git a/plugins/Live/Controller.php b/plugins/Live/Controller.php index 3e708d4d79822aa1e1bcfc0fec5b3628a082acad..f1ee8bda2368bded98b45b6ef4e0317984537ffa 100644 --- a/plugins/Live/Controller.php +++ b/plugins/Live/Controller.php @@ -99,6 +99,7 @@ class Piwik_Live_Controller extends Controller */ public function getVisitorLog($fetch = false) { + $test = array(); $str = (string)$test; return $this->getLastVisitsDetails($fetch); } @@ -130,4 +131,18 @@ class Piwik_Live_Controller extends Controller $view->pisToday = $today['actions']; return $view; } + + /** + * TODO + */ + public function getVisitorProfilePopup() + { + $idSite = Common::getRequestVar('idSite', null, 'int'); + + $view = new View('@Live/getVisitorProfilePopup.twig'); + $view->idSite = $idSite; + $view->goals = Piwik_Goals_API::getInstance()->getGoals($idSite); + $view->visitorData = Request::processRequest('Live.getVisitorProfile'); + echo $view->render(); + } } \ No newline at end of file diff --git a/plugins/Live/Live.php b/plugins/Live/Live.php index e4c15c1ed71fcdaf80e703d96f02a87f4f31f7f3..54820cd83670263554b7cbf2dac9eb17bf79be38 100644 --- a/plugins/Live/Live.php +++ b/plugins/Live/Live.php @@ -35,6 +35,7 @@ class Piwik_Live extends Plugin public function getCssFiles(&$cssFiles) { $cssFiles[] = "plugins/Live/stylesheets/live.less"; + $cssFiles[] = "plugins/Live/stylesheets/visitor_profile.less"; } public function getJsFiles(&$jsFiles) @@ -89,4 +90,4 @@ class Piwik_Live extends Plugin ) ); } -} +} \ No newline at end of file diff --git a/plugins/Live/images/REMOVE_ME_avatar.jpg b/plugins/Live/images/REMOVE_ME_avatar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dfd7ec81de6db91cd38339e32f20a87dabfc0007 Binary files /dev/null and b/plugins/Live/images/REMOVE_ME_avatar.jpg differ diff --git a/plugins/Live/images/REMOVE_ME_chart.png b/plugins/Live/images/REMOVE_ME_chart.png new file mode 100644 index 0000000000000000000000000000000000000000..5f8f03ad6aeca09d86ece46f4b30ea9c5c9bb894 Binary files /dev/null and b/plugins/Live/images/REMOVE_ME_chart.png differ diff --git a/plugins/Live/images/REMOVE_ME_map.jpg b/plugins/Live/images/REMOVE_ME_map.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ffae900c21d7544407091f8d3c38afb51fdc0f5d Binary files /dev/null and b/plugins/Live/images/REMOVE_ME_map.jpg differ diff --git a/plugins/Live/images/avatar_frame.png b/plugins/Live/images/avatar_frame.png new file mode 100644 index 0000000000000000000000000000000000000000..23eccfd5fb39ef796decc966c5792dfc3adac6ba Binary files /dev/null and b/plugins/Live/images/avatar_frame.png differ diff --git a/plugins/Live/images/paperclip.png b/plugins/Live/images/paperclip.png new file mode 100644 index 0000000000000000000000000000000000000000..b38d33caa17cf5870dea12a3bf2a8cd4847e3bcb Binary files /dev/null and b/plugins/Live/images/paperclip.png differ diff --git a/plugins/Live/images/visitor_profile_background.jpg b/plugins/Live/images/visitor_profile_background.jpg new file mode 100644 index 0000000000000000000000000000000000000000..082d637dcf661814ff38ca4e158448ae7cae5d6e Binary files /dev/null and b/plugins/Live/images/visitor_profile_background.jpg differ diff --git a/plugins/Live/images/visitor_profile_close.png b/plugins/Live/images/visitor_profile_close.png new file mode 100644 index 0000000000000000000000000000000000000000..ae132b7b2c2544bff0ad01487b2fa04cbd645e27 Binary files /dev/null and b/plugins/Live/images/visitor_profile_close.png differ diff --git a/plugins/Live/images/visitor_profile_gradient.png b/plugins/Live/images/visitor_profile_gradient.png new file mode 100644 index 0000000000000000000000000000000000000000..ac5068b54df93010f9b8f9ed84b82161ed696d1d Binary files /dev/null and b/plugins/Live/images/visitor_profile_gradient.png differ diff --git a/plugins/Live/stylesheets/visitor_profile.less b/plugins/Live/stylesheets/visitor_profile.less new file mode 100644 index 0000000000000000000000000000000000000000..4b34c32698dd3d085799da66642ea20b85f8efee --- /dev/null +++ b/plugins/Live/stylesheets/visitor_profile.less @@ -0,0 +1,365 @@ +.visitor-profile { + position:relative; + width:1149px; + height:758px; + border:1px solid #a19e96; + border-radius:5px; + background:url(../images/visitor_profile_background.jpg) repeat; + box-shadow:5px 5px 5px rgba(0,0,0,0.22); + + h1 { + font-size:18px; + color:#7e7363; + text-shadow:0 1px 0 rgba(255,255,255,1); + margin:9px 0 0 0; + padding:0; + + a { + font-size:12px; + margin-left:3px; + } + } + + span, strong { + display:inline-block; + font-size:14px; + color:#5e5e5c; + line-height:19px; + padding-left:4px; + } + + p { + font-size:13px; + color:#5e5e5c; + line-height:20px; + } + + h2 { + display:inline-block; + font-size:14px; + margin:0 0 0 5px; + padding:0; + font-weight:bold; + color:black; + } + + // TODO: iOS icon looks better in mockup + // TODO: remove temporary images +} + +.visitor-profile-close { + position:absolute; + right:-17px; + top:-16px; + height:35px; + width:35px; + background:url(../images/visitor_profile_close.png) no-repeat; +} + +.visitor-profile a { + text-decoration:none; + color:#255792; +} + +.visitor-profile > div { + width:100%; +} + +.visitor-profile-info { + height:705px; + border-top:2px solid #f6f6f6; + border-bottom:1px solid #d1cec8; + border-radius:5px 5px 0 0; + box-shadow:inset 0 25px 15px -10px #e0e0e0, inset 0 -25px 15px -10px #e0e0e0; + + > div { + width:573px; + height:100%; + float:left; + border-left:1px solid #d1cec8; + + > div { + border-bottom:1px solid #d1cec8; + } + } + + > div:first-child { + border-left:none; + } + + > div:last-child { + border-bottom:none; + } +} + +.visitor-profile-avatar > div { + position:relative; + float:left; + height:145px; + margin-right:15px; + padding:12px 0 13px; +} + +.visitor-profile-avatar > div:first-child { + width:166px; + margin-right:0; + padding-left:16px; + + > .visitor-profile-image-frame { + width:149px; + height:154px; + background:url(../images/avatar_frame.png) no-repeat; + + > img { // avatar image + width:122px; + height:120px; + margin:11px 0 0 12px; + } + } + + > img { // paperclip image + position:absolute; + top:-8px; + left:3px; + z-index:2; + } +} + +.visitor-profile-avatar > div:last-child { + margin-right:0; +} + +.visitor-profile-more-info { + height:18px; + border-top:1px solid #fff; + border-radius:0 0 5px 5px; + text-align:center; + padding:20px 0 13px; + + > a { + font-size:14px; + text-decoration:none; + color:#255792; + text-shadow:0 1px 0 rgba(255,255,255,1); + } +} + +.visitor-profile-latest-visit-column { + padding-top:6px; + display:inline-block; + vertical-align:top; +} + +.visitor-profile-browser { + width:75px; + display:inline-block; +} + +.visitor-profile-os { + width:75px; + display:inline-block; +} + +.visitor-profile-latest-visit-column:first-child { + margin-right:9px; +} + +.visitor-profile-avatar ul { + width:178px; +} + +.visitor-profile-avatar ul li { + display:inline-block; + height:24px; + border-bottom:1px solid #d1cec8; + width:100%; +} + +.visitor-profile-avatar ul li:last-child { + border-bottom:none; +} + +.visitor-profile-avatar ul li:first-child { + border-bottom:1px solid #d1cec8; // make sure there is a border if only one item is shown in the list +} + +.visitor-profile-map { + padding:19px 0 18px 19px; +} + +.visitor-profile-map * { + border-radius:2px; + background-color:#fff; + width:532px; + height:243px; + padding:2px; +} + +.visitor-profile-summary,.visitor-profile-important-visits { + overflow:hidden; + height:116px; + padding:5px 0 0 22px; +} + +.visitor-profile-summary > div { + margin-top:6px; +} + +.visitor-profile-important-visits { + + > div { + float:left; + width:265px; + height:100%; + + > div { + margin-top:13px; + } + } + + span { + padding-left:0; + } +} + +.visitor-profile-location { + padding:10px 0 4px 19px; + + img { + margin-top:-12px; + } +} + +.visitor-profile-pages-visited { + height:42px; + overflow-y:auto; + position:relative; + margin-right:10px; + border-bottom:none!important; + padding:8px 18px 10px 13px; + + h1 { + margin-left:6px; + } +} + +.visitor-profile-actions { + height:460px; + overflow-y:auto; + position:relative; + margin-right:10px; + border-bottom:none!important; + padding:0 18px 0 13px; + + ol { + counter-reset:item; + list-style-type:none; + + > li { + display:block; + font-size:12px; + font-weight:700; + line-height:25px; + padding:0 0 10px 13px; + + span { + font-size:13px; + font-weight:700; + line-height:25px; + padding-left:0; + } + } + + > li:before { + content:counter(item) " "; + counter-increment:item; + } + } + + // TODO: unordered lists no longer used, remove + ol li ul,ol li ol { + border-top:1px solid #d1cec8; + } + + ol li ul { + padding-left:15px; + } + + ol > li > ol > li { + margin-left:-12px; + } + + ol li ul li,ol li ol li { + display:block; + color:#5e5e5c; + font-size:13px; + line-height:22px; + padding-top:1px; + padding-bottom:1px; + } + + ol li ol li { + padding-bottom:4px; + } + + ol li ul li a,ol li ol li a { + display:inline-block; + } + + ol > li ol li span { + padding-left:4px; + } + + ol > li ol li .action-list-url { + margin-left:15px; + line-height:14px; + display:inline-block; + } + + ol > li ol li img { + margin-left:7px; + } + + // overrides for _actionsDetails styles + strong { + font-size:13px; + line-height:25px; + } +} + +.visitor-profile-date { + float:right; + font-size:13px; + line-height:26px; +} + +.visitor-profile-fog { + height:25px; + width:546px; + position:absolute; + bottom:51px; + right:28px; + background:url(../images/visitor_profile_gradient.png) repeat-x; +} + +// popup css +.visitor-profile-popup { + width: 1151px; + height: auto; + padding: 0; + + > .ui-dialog-titlebar { + display: none; + } + + > #Piwik_Popover { + padding: 0; + margin: 0; + overflow: visible; + } +} + +.visitor-profile-goal-name { + font-weight:bold; + font-style:italic; +} \ No newline at end of file diff --git a/plugins/Live/templates/_actionsList.twig b/plugins/Live/templates/_actionsList.twig new file mode 100644 index 0000000000000000000000000000000000000000..9f5bc672e4e01c5a9c91367425b22e29e443bd80 --- /dev/null +++ b/plugins/Live/templates/_actionsList.twig @@ -0,0 +1,106 @@ +{% set visitorHasSomeEcommerceActivity %}0{% endset %} +{% for action in actionDetails %} + {% set customVariablesTooltip %} + {% if action.customVariables is defined %} + {{ 'CustomVariables_CustomVariables'|translate }} + {% for id,customVariable in action.customVariables %} + {% set name = 'customVariablePageName' ~ id %} + {% set value = 'customVariablePageValue' ~ id %} + - {{ customVariable[name]|raw }} {% if customVariable[value]|length > 0 %} = {{ customVariable[value]|raw }}{% endif %} + {% endfor %} + {% endif %} + {% endset %} + {% if not javascriptVariablesToSet.filterEcommerce or action.type == 'ecommerceOrder' or action.type == 'ecommerceAbandonedCart' %} + <li class="{% if action.goalName is defined %}goal{% else %}action{% endif %}" + title="{{ action.serverTimePretty }}{% if action.url is defined and action.url|trim|length %} + {{ action.url }}{% endif %} {% if customVariablesTooltip|trim|length %} + + {{ customVariablesTooltip|trim }}{% endif %}{% if action.timeSpentPretty is defined %} + + {{ 'General_TimeOnPage'|translate }}: {{ action.timeSpentPretty|raw }}{% endif %}{% if action.generationTime is defined %} + + {{ 'General_ColumnGenerationTime'|translate }}: {{ action.generationTime|raw }}{% endif %}"> + {% if action.type == 'ecommerceOrder' or action.type == 'ecommerceAbandonedCart' %} + {# Ecommerce Abandoned Cart / Ecommerce Order #} + <img src="{{ action.icon }}"/> + {% if action.type == 'ecommerceOrder' %} + {% set visitorHasSomeEcommerceActivity %}1{% endset %} + <strong>{{ 'Goals_EcommerceOrder'|translate }}</strong> + <span style='color:#666666'>({{ action.orderId }})</span> + {% else %} + <strong>{{'Goals_AbandonedCart'|translate}}</strong> + + {# TODO: would be nice to have the icons Orders / Cart in the ecommerce log footer #} + {% if javascriptVariablesToSet.filterEcommerce == 2 %} + {% set visitorHasSomeEcommerceActivity %}1{% endset %} + {% endif %} + {% endif %} + <br/> + <span {% if not isWidget %}style='margin-left:20px'{% endif %}> + {% if action.type == 'ecommerceOrder' %} + <abbr title=" + {{ 'Live_GoalRevenue'|translate }}: {{ action.revenue|money(javascriptVariablesToSet.idSite)|raw }} + {% if action.revenueSubTotal is not empty %} - {{ 'General_Subtotal'|translate }}: {{ action.revenueSubTotal|money(javascriptVariablesToSet.idSite)|raw }}{% endif %} + {% if action.revenueTax is not empty %} - {{ 'General_Tax'|translate }}: {{ action.revenueTax|money(javascriptVariablesToSet.idSite)|raw }}{% endif %} + {% if action.revenueShipping is not empty %} - {{ 'General_Shipping'|translate }}: {{ action.revenueShipping|money(javascriptVariablesToSet.idSite)|raw }}{% endif %} + {% if action.revenueDiscount is not empty %} - {{ 'General_Discount'|translate }}: {{ action.revenueDiscount|money(javascriptVariablesToSet.idSite)|raw }}{% endif %} + ">{{ 'Live_GoalRevenue'|translate }}: + {% else %} + {% set revenueLeft %}{{ 'Live_GoalRevenue'|translate }}{% endset %} + {{ 'Goals_LeftInCart'|translate(revenueLeft) }}: + {% endif %} + <strong>{{ action.revenue|money(javascriptVariablesToSet.idSite)|raw }}</strong> + {% if action.type == 'ecommerceOrder' %} + </abbr> + {% endif %}, {{ 'General_Quantity'|translate }}: {{ action.items }} + + {# Ecommerce items in Cart/Order #} + {% if action.itemDetails is not empty %} + <ul style='list-style:square;margin-left:{% if isWidget %}15{% else %}50{% endif %}px'> + {% for product in action.itemDetails %} + <li> + {{ product.itemSKU }}{% if product.itemName is not empty %}: {{ product.itemName }}{% endif %} + {% if product.itemCategory is not empty %} ({{ product.itemCategory }}){% endif %} + , + {{ 'General_Quantity'|translate }}: {{ product.quantity }}, + {{ 'General_Price'|translate }}: {{ product.price|money(javascriptVariablesToSet.idSite)|raw }} + </li> + {% endfor %} + </ul> + {% endif %} + </span> + {% elseif action.goalName is not defined%} + {# Page view / Download / Outlink #} + {% if action.pageTitle is defined and action.pageTitle is not empty %} + <span>{{ action.pageTitle|truncate(80) }}</span> + {% endif %} + {% if action.siteSearchKeyword is defined %} + {% if action.type == 'search' %} + <img src='{{ action.icon }}' title='{{ 'Actions_SubmenuSitesearch'|translate }}'> + {% endif %} + {{ action.siteSearchKeyword|truncate(80) }} + {% endif %} + {% if action.url is not empty %} + {% if action.type == 'action' and action.pageTitle is not empty %}<br/>{% endif %} + {% if action.type == 'download' or action.type == 'outlink' %} + <img src='{{ action.icon }}'> + {% endif %} + <a href="{{ action.url }}" target="_blank" class="action-list-url" + {% if overrideLinkStyle is not defined or overrideLinkStyle %}style="{% if action.type=='action' and action.pageTitle is not empty %}margin-left: 25px;{% endif %}text-decoration:underline;"{% endif %}> + {{ action.url|truncate(80) }} + </a> + {% elseif action.type != 'search' %} + <br/> + <span style="margin-left: 25px;">{{ javascriptVariablesToSet.pageUrlNotDefined }}</span> + {% endif %} + {% else %} + {# Goal conversion #} + <img src="{{ action.icon }}" /> + <strong>{{ action.goalName }}</strong> + {% if action.revenue > 0 %}, {{ 'Live_GoalRevenue'|translate }}: + <strong>{{ action.revenue|money(javascriptVariablesToSet.idSite)|raw }}</strong> + {% endif %} + {% endif %} + </li> + {% endif %} +{% endfor %} \ No newline at end of file diff --git a/plugins/Live/templates/getVisitorLog.twig b/plugins/Live/templates/getVisitorLog.twig index 953528ac2fa747156fd2b3a19d97f3a07608c32b..211d1d0cb38948c9bdb3e7a1b7726af1b9f45d1c 100644 --- a/plugins/Live/templates/getVisitorLog.twig +++ b/plugins/Live/templates/getVisitorLog.twig @@ -187,112 +187,7 @@ </strong> <br/> <ol class='visitorLog'> - {% set visitorHasSomeEcommerceActivity %}0{% endset %} - {% for action in visitor.getColumn('actionDetails') %} - {% set customVariablesTooltip %} - {% if action.customVariables is defined %} - {{ 'CustomVariables_CustomVariables'|translate }} - {% for id,customVariable in action.customVariables %} - {% set name = 'customVariablePageName' ~ id %} - {% set value = 'customVariablePageValue' ~ id %} - - {{ customVariable[name]|raw }} {% if customVariable[value]|length > 0 %} = {{ customVariable[value]|raw }}{% endif %} - {% endfor %} - {% endif %} - {% endset %} - {% if not javascriptVariablesToSet.filterEcommerce or action.type == 'ecommerceOrder' or action.type == 'ecommerceAbandonedCart' %} - <li class="{% if action.goalName is defined %}goal{% else %}action{% endif %}" - title="{{ action.serverTimePretty }}{% if action.url is defined and action.url|trim|length %} - {{ action.url }}{% endif %} {% if customVariablesTooltip|trim|length %} - - {{ customVariablesTooltip|trim }}{% endif %}{% if action.timeSpentPretty is defined %} - - {{ 'General_TimeOnPage'|translate }}: {{ action.timeSpentPretty|raw }}{% endif %}{% if action.generationTime is defined %} - - {{ 'General_ColumnGenerationTime'|translate }}: {{ action.generationTime|raw }}{% endif %}"> - {% if action.type == 'ecommerceOrder' or action.type == 'ecommerceAbandonedCart' %} - {# Ecommerce Abandoned Cart / Ecommerce Order #} - <img src="{{ action.icon }}"/> - {% if action.type == 'ecommerceOrder' %} - {% set visitorHasSomeEcommerceActivity %}1{% endset %} - <strong>{{ 'Goals_EcommerceOrder'|translate }}</strong> - <span style='color:#666666'>({{ action.orderId }})</span> - {% else %} - <strong>{{'Goals_AbandonedCart'|translate}}</strong> - - {# TODO: would be nice to have the icons Orders / Cart in the ecommerce log footer #} - {% if javascriptVariablesToSet.filterEcommerce == 2 %} - {% set visitorHasSomeEcommerceActivity %}1{% endset %} - {% endif %} - {% endif %} - <br/> - <span {% if not isWidget %}style='margin-left:20px'{% endif %}> - {% if action.type == 'ecommerceOrder' %} - <abbr title=" - {{ 'Live_GoalRevenue'|translate }}: {{ action.revenue|money(javascriptVariablesToSet.idSite)|raw }} - {% if action.revenueSubTotal is not empty %} - {{ 'General_Subtotal'|translate }}: {{ action.revenueSubTotal|money(javascriptVariablesToSet.idSite)|raw }}{% endif %} - {% if action.revenueTax is not empty %} - {{ 'General_Tax'|translate }}: {{ action.revenueTax|money(javascriptVariablesToSet.idSite)|raw }}{% endif %} - {% if action.revenueShipping is not empty %} - {{ 'General_Shipping'|translate }}: {{ action.revenueShipping|money(javascriptVariablesToSet.idSite)|raw }}{% endif %} - {% if action.revenueDiscount is not empty %} - {{ 'General_Discount'|translate }}: {{ action.revenueDiscount|money(javascriptVariablesToSet.idSite)|raw }}{% endif %} - ">{{ 'Live_GoalRevenue'|translate }}: - {% else %} - {% set revenueLeft %}{{ 'Live_GoalRevenue'|translate }}{% endset %} - {{ 'Goals_LeftInCart'|translate(revenueLeft) }}: - {% endif %} - <strong>{{ action.revenue|money(javascriptVariablesToSet.idSite)|raw }}</strong> - {% if action.type == 'ecommerceOrder' %} - </abbr> - {% endif %}, {{ 'General_Quantity'|translate }}: {{ action.items }} - - {# Ecommerce items in Cart/Order #} - {% if action.itemDetails is not empty %} - <ul style='list-style:square;margin-left:{% if isWidget %}15{% else %}50{% endif %}px'> - {% for product in action.itemDetails %} - <li> - {{ product.itemSKU }}{% if product.itemName is not empty %}: {{ product.itemName }}{% endif %} - {% if product.itemCategory is not empty %} ({{ product.itemCategory }}){% endif %} - , - {{ 'General_Quantity'|translate }}: {{ product.quantity }}, - {{ 'General_Price'|translate }}: {{ product.price|money(javascriptVariablesToSet.idSite)|raw }} - </li> - {% endfor %} - </ul> - {% endif %} - </span> - {% elseif action.goalName is not defined%} - {# Page view / Download / Outlink #} - {% if action.pageTitle is defined %} - {{ action.pageTitle|truncate(80) }} - {% endif %} - {% if action.siteSearchKeyword is defined %} - {% if action.type == 'search' %} - <img src='{{ action.icon }}' title='{{ 'Actions_SubmenuSitesearch'|translate }}'> - {% endif %} - {{ action.siteSearchKeyword|truncate(80) }} - {% endif %} - {% if action.url is not empty %} - {% if action.type == 'action' and action.pageTitle is not empty %}<br/>{% endif %} - {% if action.type == 'download' or action.type == 'outlink' %} - <img src='{{ action.icon }}'> - {% endif %} - <a href="{{ action.url }}" target="_blank" - style="{% if action.type=='action' and action.pageTitle is not empty %}margin-left: 25px;{% endif %}text-decoration:underline;"> - {{ action.url|truncate(80) }} - </a> - {% elseif action.type != 'search' %} - <br/> - <span style="margin-left: 25px;">{{ javascriptVariablesToSet.pageUrlNotDefined }}</span> - {% endif %} - {% else %} - {# Goal conversion #} - <img src="{{ action.icon }}" /> - <strong>{{ action.goalName }}</strong> - {% if action.revenue > 0 %}, {{ 'Live_GoalRevenue'|translate }}: - <strong>{{ action.revenue|money(javascriptVariablesToSet.idSite)|raw }}</strong> - {% endif %} - {% endif %} - </li> - {% endif %} - {% endfor %} + {% include "@Live/_actionsList.twig" with {'actionDetails': visitor.getColumn('actionDetails')} %} </ol> </td> </tr> @@ -317,8 +212,8 @@ var visitorLogTitle = '{{ 'Live_VisitorLog'|translate|e('js') }}'; function Piwik_Live_LoadVisitorPopover(visitorId) { var startingDate = piwik.minDateYear + '-01-01'; - var url = 'module=Live&action=getVisitorLog&period=range&date=' + startingDate + ',today&show_footer=0&segment=visitorId' + encodeURIComponent('==') + visitorId; - return Piwik_Popover.createPopupAndLoadUrl(url, visitorLogTitle); + var url = 'module=Live&action=getVisitorProfilePopup&period=range&date=' + startingDate + ',today&idVisitor=' + encodeURIComponent(visitorId); + return Piwik_Popover.createPopupAndLoadUrl(url, visitorLogTitle, 'visitor-profile-popup'); } $(document).ready(function () { diff --git a/plugins/Live/templates/getVisitorProfilePopup.twig b/plugins/Live/templates/getVisitorProfilePopup.twig new file mode 100644 index 0000000000000000000000000000000000000000..8fe3ef39e2ec6d6c0d5100ffed08e27145ca5e4f --- /dev/null +++ b/plugins/Live/templates/getVisitorProfilePopup.twig @@ -0,0 +1,121 @@ +<div class="visitor-profile"> + <a href class="visitor-profile-close"></a> + <div class="visitor-profile-info"> + <div> + <div class="visitor-profile-avatar"> + <div> + <div class="visitor-profile-image-frame"><!-- TODO translate --> + <img src="plugins/Live/images/REMOVE_ME_avatar.jpg" alt=""/> + </div> + <img src="plugins/Live/images/paperclip.png" alt=""/> + </div> + <div> + <h1>Visitor profile</h1> + <div> + <div class="visitor-profile-latest-visit-column"> + <ul> + <li><span>IP</span><strong>{{ visitorData.latestVisitIp }}</strong></li> + <li><span>ID</span><strong>{{ visitorData.visitorId }}</strong></li> + <li> + <div class="visitor-profile-browser"> + <img src="{{ visitorData.browserLogo }}"/><span>{{ visitorData.browserName }}</span> + </div> + <div class="visitor-profile-os"> + <img src="{{ visitorData.operatingSystemLogo }}"/><span>{{ visitorData.operatingSystemShortName }}</span> + </div> + </li> + <li><span>Resolution</span><strong>{{ visitorData.resolution }}</strong></li> + </ul> + </div> + <div class="visitor-profile-latest-visit-column"> + <ul> + {% for id,customVariable in visitorData.customVariables %} + {% if loop.index0 < 4 %} + {% set name='customVariableName' ~ id %} + {% set value='customVariableValue' ~ id %} + <li><span>{{ customVariable[name]|truncate(30) }}</span>{% if customVariable[value]|length > 0 %}<strong>{{ customVariable[value]|truncate(50) }}</strong>{% endif %}</li> + {% endif %} + {% endfor %} + {# TODO: Other custom vars go in expanding div. #} + </ul> + </div> + </div> + </div> + <p style="clear:both; border:none!important;"></p> + </div> + <div style="clear:both; border:none!important;"></div> + <div class="visitor-profile-map"> + <img alt="" src="plugins/Live/images/REMOVE_ME_map.jpg"/> {# TODO: map #} + </div> + <div class="visitor-profile-important-visits"> + <div> + <h1>First visit</h1> + <div> + <p><strong>{{ visitorData.firstVisit.prettyDate }}</strong><span> - {{ visitorData.firstVisit.daysAgo }} days ago</span></p> + <p><span>from:</span></p> + <p><strong>{{ visitorData.firstVisit.referralSummary }}</strong></p> + </div> + </div> + <div> + <h1>Last visit</h1> + <div> + <p><strong>{{ visitorData.lastVisit.prettyDate }}</strong><span> - {{ visitorData.lastVisit.daysAgo }} days ago</span></p> + <p><span>from:</span></p> + <p><strong>{{ visitorData.lastVisit.referralSummary }}</strong></p> + </div> + </div> + </div> + <div class="visitor-profile-summary"> + <h1>Summary</h1> + <div> + <p>Spent a total of <strong>{{ visitorData.totalVisitDurationPretty|raw }} on the website</strong>, and <strong>viewed {{ visitorData.totalActionCount }} pages in {{ visitorData.totalVisits }} visits.</strong></p> + <p><strong>Converted {{ visitorData.totalGoalConversions }} Goals</strong> ( + {%- for idGoal, totalConversions in visitorData.totalConversionsByGoal -%} + {%- if not loop.first %}, {% endif -%}{{- totalConversions }} <span class="visitor-profile-goal-name">{{ goals[idGoal]['name'] -}}</span> + {%- endfor -%} + ).</p> + <p>Ecommerce: <strong>{{ visitorData.totalEcommerceConversions }} orders for a total of {{ visitorData.totalEcommerceRevenue|money(idSite)|raw }}</strong>, purchased {{ visitorData.totalEcommerceItems }} items.</p> + </div> + </div> + </div> + <div> + <div class="visitor-profile-location"> + <h1>Location</h1> + <img src="plugins/Live/images/REMOVE_ME_chart.png" alt=""/> {# TODO: country & bar graph #} + </div> + <div class="visitor-profile-pages-visited"> + <h1>Visited pages<a href>see all</a></h1> + </div> + <div class="visitor-profile-actions"> + <ol> + {% for visitInfo in visitorData.lastVisits.getRows() %} + <li><h2>Visit</h2><span class="visitor-profile-date">{{ visitInfo.getColumn('serverDatePrettyFirstAction') }}</span> + <ol> + {% include "@Live/_actionsList.twig" with {'actionDetails': visitInfo.getColumn('actionDetails'), + 'javascriptVariablesToSet': { + 'filterEcommerce': false, + 'idSite': idSite + }, + 'overrideLinkStyle': false} %} + </ol> + </li> + {% endfor %} + </ol> + <br/> + </div> + <div class="visitor-profile-fog"></div> + </div> + </div> + <div class="visitor-profile-more-info"> + <a href="#">View more visitor information</a> + </div> +</div> +<script type="text/javascript"> +$(function() { + $('.visitor-profile-actions').jScrollPane({ + showArrows: true, + verticalArrowPositions: 'os', + horizontalArrowPositions: 'os' + }); +}); +</script> \ No newline at end of file diff --git a/plugins/SegmentEditor/SegmentEditor.php b/plugins/SegmentEditor/SegmentEditor.php index 5d1731c0063ef3d9ca527f105ad5d0a211059660..48359598a6ec69a81f16126559d845d7ebe87e70 100644 --- a/plugins/SegmentEditor/SegmentEditor.php +++ b/plugins/SegmentEditor/SegmentEditor.php @@ -97,16 +97,11 @@ class Piwik_SegmentEditor extends Plugin public function getJsFiles(&$jsFiles) { - $jsFiles[] = "plugins/SegmentEditor/javascripts/jquery.jscrollpane.js"; $jsFiles[] = "plugins/SegmentEditor/javascripts/Segmentation.js"; - $jsFiles[] = "plugins/SegmentEditor/javascripts/jquery.mousewheel.js"; - $jsFiles[] = "plugins/SegmentEditor/javascripts/mwheelIntent.js"; } public function getCssFiles(&$cssFiles) { $cssFiles[] = "plugins/SegmentEditor/stylesheets/segmentation.less"; - $cssFiles[] = "plugins/SegmentEditor/stylesheets/jquery.jscrollpane.css"; - $cssFiles[] = "plugins/SegmentEditor/stylesheets/scroll.less"; } }