]> _ Git - fluidbook-toolbox.git/commitdiff
wip #5877 @2
authorVincent Vanwaelscappel <vincent@cubedesigners.com>
Fri, 9 Jun 2023 14:37:22 +0000 (16:37 +0200)
committerVincent Vanwaelscappel <vincent@cubedesigners.com>
Fri, 9 Jun 2023 14:37:22 +0000 (16:37 +0200)
app/Fluidbook/Stats.php
app/Http/Controllers/Admin/Operations/FluidbookPublication/StatsOperation.php
resources/views/fluidbook_stats/loader.blade.php
resources/views/fluidbook_stats/summary.blade.php

index aea014eebd0a9de801a2dac08c566a0dce1cc05d..fda57ad221a0a4f936c193b8711c486361710443 100644 (file)
@@ -333,6 +333,7 @@ class Stats extends Reporting
                 'formatted_date' => self::getPeriods()[$this->getPeriod()]['singular'],
                 'nb_uniq_visitors' => __('Visiteurs uniques'),
                 'nb_visits' => __('Visites'),
+                'nb_openings' => __('Ouvertures'),
                 'nb_hits' => __('Pages vues'),
                 'nb_links' => __('Liens sortants'),
                 'nb_downloads' => __('Téléchargements'),
@@ -357,7 +358,7 @@ class Stats extends Reporting
 
     }
 
-    protected function _supportUniqueVisitors()
+    protected function _supportUniqueVisitors(): bool
     {
         return $this->fluidbook_id >= self::SHOW_VISITORS_CUTOFF && !in_array($this->getPeriod(), ['year', 'range']);
     }
index 55056d2e816ca770158089e94f6042aa18cce045..3de818811503d72f0f662d3fe454320810600f1f 100644 (file)
@@ -20,17 +20,10 @@ trait StatsOperation
             ->withoutMiddleware([CheckIfAdmin::class])->name('statsmatomo'); // Named route is used to generate URLs more consistently using route helper
 
         // Main route is only secured by hash (security by obscurity)
-        Route::get($segment . '/stats/{fluidbook_id}_{hash}/{date?}/{period_override?}', $controller . '@statsLoader')
+        Route::get($segment . '/stats/{fluidbook_id}_{hash}/{date?}/{period_override?}', $controller . '@statsSummary')
             ->withoutMiddleware([CheckIfAdmin::class])
             ->name('stats'); // Named route is used to generate URLs more consistently using route helper
 
-
-        // Since the stats can sometimes be slow to generate, the initial URL displays
-        // a loader while the actual report is fetched via AJAX and injected into the page.
-        Route::get($segment . '/stats-data/{fluidbook_id}_{hash}/{date?}/{period_override?}', $controller . '@statsSummary')
-            ->withoutMiddleware([CheckIfAdmin::class])
-            ->name('stats-report');
-
         // API testing tool (intended for superadmins only)
         Route::get($segment . '/stats/API/{id?}', $controller . '@statsAPI');
     }
@@ -40,13 +33,6 @@ trait StatsOperation
         $this->crud->addButtonFromView('line', 'stats', 'fluidbook_publication.stats', 'end');
     }
 
-
-    protected function statsLoader($fluidbook_id, $hash, $date = null, $period_override = null)
-    {
-        $report_URL = route('stats-report', compact('fluidbook_id', 'hash', 'date', 'period_override'));
-        return view('fluidbook_stats.loader', compact('report_URL'));
-    }
-
     protected function statsSummary($fluidbook_id, $hash, $date = null, $period_override = null)
     {
         $fluidbook = FluidbookPublication::withoutGlobalScopes()->where('id', $fluidbook_id)->where('hash', $hash)->firstOrFail();
@@ -54,7 +40,7 @@ trait StatsOperation
         $data = $stats->processData($period_override);
         $data['locale'] = app()->getLocale();
         $data['fluidbook'] = $fluidbook;
-        return view('fluidbook_stats.summary', $data);
+        return view('fluidbook_stats.loader', $data);
 
     }
 
index 75107959f864164bf23c04562003837ae8d6567d..5d8b5ebeba7f59b1118e383dc18b4c68cdf78250 100644 (file)
 {{-- __('!! Statistiques') --}}
 @extends(backpack_view('blank'))
+@push('after_scripts')
+    {{-- Include the base libraries for the chart and the date picker here so that they're ready
+    when the report HTML and extra scripts are injected --}}
 
-@section('after_styles')
-    <link rel="stylesheet" href="{{ asset('packages/daterangepicker/daterangepicker.css') }}">
-    <style>
-        .container-fluid.animated.fadeIn {
-            {{-- Disable entrance animation on this page because we already have the stats loader --}}
-            animation: none;
-        }
-
-        @keyframes spinner {
-            to {
-                transform: rotate(360deg);
-            }
-        }
+    {{-- Charting library --}}
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"
+            integrity="sha512-ElRFoEQdI5Ht6kZvyzXhYG9NqjtkmlkfYk0wr6wHxU9JEHakS7UJZNeml5ALk+8IKlU6jDgMabC3vkumRokgJA=="
+            crossorigin="anonymous" referrerpolicy="no-referrer"></script>
 
-        #stats_loader:before {
-            content: '';
-            box-sizing: border-box;
-            width: 3em;
-            height: 3em;
-            border-radius: 50%;
-            border: 2px solid #ccc;
-            border-top-color: #1b2a4e;
-            animation: spinner .6s linear infinite;
-        }
+    {{-- Date Range picker: https://sensortower.github.io/daterangepicker/docs --}}
+    <script type="text/javascript" src="{{ asset('packages/moment/min/moment-with-locales.min.js') }}"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.min.js"
+            integrity="sha512-vs7+jbztHoMto5Yd/yinM4/y2DOkPLt0fATcN+j+G4ANY2z4faIzZIOMkpBmWdcxt+596FemCh9M18NUJTZwvw=="
+            crossorigin="anonymous" referrerpolicy="no-referrer"></script>
+    <script type="text/javascript" src="{{ asset('packages/daterangepicker/daterangepicker.js') }}"></script>
 
-        #stats_loader {
-            display: flex;
-            min-height: 50vh;
-            align-items: center;
-            justify-content: center;
-            flex-direction: column;
-            gap: 1em;
-            padding: 2em;
-            color: #aaa;
-            font-size: 20px;
-        }
-
-        #stats_error {
-            background-color: #fff5f5;
-            color: #c53030;
-            border: 1px solid #fc8181;
-            border-radius: 0.25em;
-            padding: 0.75em 1em;
-            display: block;
-            width: max-content;
-            margin: 5em auto;
-        }
+    {{-- Simple Table Sorter --}}
+    <script type="text/javascript" src="{{ asset('packages/sorttable/sorttable.js') }}"></script>
+    {{-- This script works on any tables with the "sortable" class. There's no extra setup needed here. --}}
+@endpush
 
+@section('after_styles')
+    <link rel="stylesheet" href="{{ asset('packages/daterangepicker/daterangepicker.css') }}">
+    <style>
         .chart-header {
             display: flex;
             align-items: center;
             justify-content: space-between;
         }
 
-        .periods {
-            display: inline-flex;
-            margin-left: 2em;
-        }
-        .periods > * {
-            padding: 0.35em 0.7em;
-            border: 1px solid #ddd;
-            line-height: 1;
-        }
-        .periods > * + * {
-            border-left: 0;
-        }
-        .periods span {
-            background-color: #eaeaea;
-        }
-        .periods a {
-            background-color: #f4f4f4;
-        }
-        .periods a:hover {
-            background-color: #467fcf;
-            color: #fff;
-            text-decoration: none;
-        }
-        .period_disabled {
-            opacity: 0.5;
-            cursor: not-allowed;
-        }
-
         .summary {
             display: grid;
             grid-template-columns: max-content 1fr;
         }
+
         .summary dt, .summary dd {
             margin: 0;
             padding: 0.5em;
         }
+
         .summary dt {
             display: flex;
             align-items: center;
             white-space: nowrap;
             padding-right: 2em;
         }
+
         .summary dt:nth-of-type(odd), .summary dd:nth-of-type(odd), table.stats-details tbody tr:nth-of-type(odd) {
             background-color: #f4f4f4;
         }
+
         .summary dt:nth-of-type(even), .summary dd:nth-of-type(even), table.stats-details tbody tr:nth-of-type(even) {
             background-color: #eaeaea;
         }
             width: 100%;
             table-layout: fixed;
         }
+
         table.stats-details th, table.stats-details td {
             padding: 0.5em 0.75em;
         }
+
         table.stats-details thead tr {
             position: sticky;
             top: 0;
             background-color: #fff;
         }
+
         table.stats-details tbody tr:hover td {
             background-color: rgba(156, 195, 34, 0.15);
         }
             flex-wrap: wrap;
             gap: 2em;
         }
+
         .table-columns > div {
             flex: 1;
         }
         .heading-subtitle {
             opacity: 0.6;
         }
+
         .heading-subtitle:before {
             content: ' — ';
         }
         .daterangepicker .periods li:hover, .daterangepicker .periods li.active, .daterangepicker .ranges li:hover, .daterangepicker .ranges li.active {
             background: #263340;
         }
+
         .daterangepicker .periods li, .daterangepicker .ranges li {
             color: #467fcf;
         }
             padding: 0.25em 0.5em;
             width: 11ch;
         }
+
         .daterangepicker .custom-range-inputs .arrow-right {
             opacity: 0.5;
         }
+
         .daterangepicker .custom-range-buttons {
             flex-direction: row-reverse;
         }
+
         .daterangepicker.expanded .custom-range-buttons {
             position: absolute;
             right: 1em;
             bottom: 0.75em;
         }
+
         .daterangepicker .custom-range-buttons button {
             color: #467fcf;
         }
+
         .daterangepicker .custom-range-buttons button.apply-btn {
             background: #9cc322;
         }
+
         .daterangepicker .custom-range-buttons button.apply-btn:hover {
             background: #263340;
         }
             cursor: pointer;
             white-space: nowrap;
         }
+
         .sortable th:not(.sorttable_nosort):after {
             visibility: hidden; /* only shows on actively sorted column */
             content: '';
         .sorttable_sorted_ascending:after {
             visibility: visible !important;
         }
+
         .sorttable_sorted_descending:after {
             visibility: visible !important;
             transform: rotate(180deg);
 @endsection
 
 @section('content')
-    <div id="stats_loader">Loading...</div>
-    <div id="stats_wrapper"></div>
-    <div id="stats_error" style="display: none">
-        {{ __('Une erreur s\'est produite lors de la génération du rapport') }}
+    <div id="stats_wrapper">
+        @include('fluidbook_stats.summary')
     </div>
 @endsection
-
-@push('after_scripts')
-    {{-- Fetch the report and inject it into the page when ready... This is to improve the UX --}}
-    <script>
-
-        fetch('{{ $report_URL }}').then(function (response) {
-            if (!response.ok) {
-                throw Error(`${response.statusText} (${response.status}) - ${response.url}`);
-            }
-            return response.text();
-        }).then(function (html) {
-            hideStatsLoader();
-            setInnerHTML(document.getElementById('stats_wrapper'), html);
-        }).catch(function (err) {
-            console.warn('Unable to load stats report', err);
-            showStatsError();
-        });
-
-        function hideStatsLoader() {
-            document.getElementById('stats_loader').style.display = 'none';
-        }
-
-        function showStatsLoader() {
-            document.getElementById('stats_wrapper').style.display = 'none';
-            document.getElementById('stats_loader').style.display = 'flex';
-            window.scrollTo(0,0);
-        }
-
-        function showStatsError() {
-            hideStatsLoader();
-            document.getElementById('stats_error').style.display = 'block';
-        }
-
-        // Set the inner HTML of an element AND make any script tags executable
-        // Ref: https://stackoverflow.com/a/47614491
-        function setInnerHTML(elm, html) {
-            try {
-                elm.innerHTML = html;
-            }catch (e){
-
-            }
-            Array.from(elm.querySelectorAll("script")).forEach( oldScript => {
-                const newScript = document.createElement("script");
-                Array.from(oldScript.attributes)
-                    .forEach( attr => newScript.setAttribute(attr.name, attr.value) );
-                newScript.appendChild(document.createTextNode(oldScript.innerHTML));
-                oldScript.parentNode.replaceChild(newScript, oldScript);
-            });
-        }
-
-    </script>
-
-    {{-- Include the base libraries for the chart and the date picker here so that they're ready
-    when the report HTML and extra scripts are injected --}}
-
-    {{-- Charting library --}}
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js" integrity="sha512-ElRFoEQdI5Ht6kZvyzXhYG9NqjtkmlkfYk0wr6wHxU9JEHakS7UJZNeml5ALk+8IKlU6jDgMabC3vkumRokgJA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
-
-    {{-- Date Range picker: https://sensortower.github.io/daterangepicker/docs --}}
-    <script type="text/javascript" src="{{ asset('packages/moment/min/moment-with-locales.min.js') }}"></script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.min.js" integrity="sha512-vs7+jbztHoMto5Yd/yinM4/y2DOkPLt0fATcN+j+G4ANY2z4faIzZIOMkpBmWdcxt+596FemCh9M18NUJTZwvw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
-    <script type="text/javascript" src="{{ asset('packages/daterangepicker/daterangepicker.js') }}"></script>
-@endpush
index 7330629ebde5765ff0e5ecb0abd6acf2d7b279aa..589ba1c89220e2f19c11ff06c4cb89e6b14284cb 100644 (file)
@@ -2,9 +2,12 @@
 {{-- Statistics Report --}}
 {{-- This doesn't extend any templates because it is fetched from loader.blade.php and injected via JS --}}
 
+@php
+    $tableClasses='sortable bg-white table table-striped table-hover nowrap rounded shadow-xs border-xs mt-2 dataTable dtr-inline';
+@endphp
+
 <h2 class="mt-4">
     {{ __('Statistiques') }}
-    <span class="heading-subtitle">{{ $title }}</span>
 </h2>
 
 <div data-daterangepicker class="mb-4" style="cursor: pointer"
 </div>
 
 
-<dl class="summary">
-    <dt>{{ __('Titre de la publication') }}</dt>
-    <dd>{{ $title }}</dd>
-
-    <dt>{{ __('Crée le') }}</dt>
-    <dd>
-        {{ $fluidbook->created_at->isoFormat('dddd, Do MMMM YYYY') }}
-        <span style="opacity: 0.6; display: inline-block; margin-left: 0.5em;">
+<table class="{!! $tableClasses !!}">
+    <thead style="display: none">
+    </thead>
+    <tr>
+        <td>#</td>
+        <td>{{ $fluidbook->id }}</td>
+    </tr>
+    <tr>
+        <td>{{ __('Titre de la publication') }}</td>
+        <td>{{ $title }}</td>
+    </tr>
+    <tr>
+        <td>{{ __('Crée le') }}</td>
+        <td>
+            {{ $fluidbook->created_at->isoFormat('Do MMMM YYYY') }}
+            <span style="opacity: 0.6; display: inline-block; margin-left: 0.5em;">
             ({{ $fluidbook->created_at->diffForHumans([
                     'parts' => 2, // How many levels of detail to go into (ie. years, months, days)
                     'join' => true, // Join string with natural language separators for the locale
                 ])
             }})
         </span>
-    </dd>
-
-    <dt>{{ __('Nombre de pages') }}</dt>
-    <dd>{{ $page_count }}</dd>
+        </td>
+    </tr>
+    <tr>
+        <td>{{ __('Nombre de pages') }}</td>
+        <td>{{ $page_count }}</td>
+    </tr>
 
     {{-- Summary of totals --}}
     @if($period_details->isNotEmpty())
             @php
                 if ($summary_key === 'formatted_date') continue;
             @endphp
-            <dt>{{ $summary_heading }}</dt>
-            <dd>{{ $formatter->format($period_details->sum($summary_key)) }}</dd>
+            <tr>
+                <td>{{ $summary_heading }}</td>
+                <td>{{ $formatter->format($period_details->sum($summary_key)) }}</td>
+            </tr>
         @endforeach
 
-        <dt>{{ __('Recherches') }}</dt>
-        <dd>{{ $formatter->format($searches->sum()) }}</dd>
+        <tr>
+            <td>{{ __('Recherches') }}</td>
+            <td>{{ $formatter->format($searches->sum()) }}</td>
+        </tr>
     @endif
-</dl>
-
+</table>
+<a id="chart"></a>
 @if($period_details->isNotEmpty())
 
     {{-- Period (segmentation) override controls [ Day / Week / Month / Year ] --}}
     <div class="chart-header mt-5">
+
         <h2>{!! $chart_heading !!}</h2>
-        <div class="periods">
+        <div class="btn-group m-t-10">
             @foreach($period_map as $period_key => $period_title)
-                @php
-                    // When in the month or year display modes, it makes no sense to offer the month and year segmentations
-                    $period_not_available = ($mode === 'month' && in_array($period_key, ['month', 'year'])) || ($mode === 'year' && $period_key === 'year');
-                @endphp
-                @if($period_key === $period || $period_not_available)
-                    <span @if($period_not_available)class="period_disabled"@endif>{{ $period_title['singular'] }}</span>
+                <a class="btn @if($period_key === $period)
+                btn-success
                 @else
-                    <a href="{{ route('stats', compact('fluidbook_id', 'hash') + ['date' => $date ?? '-', 'period_override' => $period_key]) }}"
-                       onclick="showStatsLoader()">
-                        {{ $period_title['singular'] }}
-                    </a>
-                @endif
+                btn-secondary
+@endif"
+                   href="{{ route('stats', compact('fluidbook_id', 'hash') + ['date' => $date ?? '-', 'period_override' => $period_key]) }}#chart">
+                    {{ $period_title['singular'] }}
+                </a>
             @endforeach
         </div>
     </div>
     </div>
 
     {{-- Stats for each period entry (year, month, week or day) --}}
-    <table class="sortable stats-details mt-5">
+    <table class="{!! $tableClasses !!}">
         <thead>
         <tr>
             @foreach ($table_map['summary'] as $summary_heading_key => $summary_heading)
-                <th @if($summary_heading_key === 'formatted_date')class="sorttable_sorted_ascending"@endif>
+                <th @if($summary_heading_key === 'formatted_date')class="sorttable_sorted_reverse"@endif>
                     {{ $summary_heading }}
                 </th>
             @endforeach
             <tr>
                 @foreach (array_keys($table_map['summary']) as $summary_key)
                     <td data-name="{{ $summary_key }}"
-                        @if($summary_key === 'formatted_date')data-sort-value="{{ $period_data['raw_date'] }}"@endif>
+                        @if($summary_key === 'formatted_date')sorttable_customkey="{{ $period_data['raw_date'] }}"@endif>
                         @if(isset($period_data[$summary_key]))
                             {!! is_int($period_data[$summary_key]) ? $formatter->format($period_data[$summary_key]) : $period_data[$summary_key] !!}
                         @else
     {{-- Stats segregated by page number --}}
     <h3 class="mt-5">{{ __('Détails par page') }} <small>({!! $formatted_date_range !!})</small></h3>
 
-    <table class="sortable stats-details mt-3">
+    <table class="{!! $tableClasses !!}">
         <thead>
         <tr>
             @foreach ($table_map['per-page'] as $page_data_heading_key => $page_data_heading)
                 {{-- In the case of the "page_group" data, we want it to be sorted in ascending order by default, even though it's a numeric column --}}
-                <th @if($page_data_heading_key === 'page_group')class="sorttable_sorted_ascending"
+                <th @if($page_data_heading_key === 'page_group')class="sorttable_sorted"
                     data-sort-direction="ascending"@endif>
                     {{ $page_data_heading }}
                 </th>
             <tr>
                 @foreach (array_keys($table_map['per-page']) as $summary_key)
                     <td data-name="{{ $summary_key }}"
-                        @if($summary_key === 'page_group')data-sort-value="{{ $page_data['page_number'] }}"@endif>
+                        @if($summary_key === 'page_group')sorttable_customkey="{{ $page_data['page_number'] }}"@endif>
                         {!! is_int($page_data[$summary_key]) ? $formatter->format($page_data[$summary_key]) : $page_data[$summary_key] !!}
                     </td>
                 @endforeach
             <div>
                 <h3>{{ __('Liens sortants') }}</h3>
 
-                <table class="sortable stats-details mt-3">
+                <table class="{!! $tableClasses !!}">
                     <thead>
                     <tr>
                         <th>{{ __('URL') }}</th>
-                        <th class="sorttable_sorted_descending">{{ __('Clics') }}</th>
+                        <th class="sorttable_sorted_reverse">{{ __('Clics') }}</th>
                     </tr>
                     </thead>
                     <tbody>
                     @foreach($outlinks as $link)
                         <tr>
-                            <td>
-                                <a href="{{ $link['label'] }}" target="_blank" rel="noopener">{{ $link['label'] }}</a>
-                            </td>
+                            <td sorttable_customkey="{{$link['label']}}">{!! \Cubist\Util\Url::linkIfisURL($link['label']) !!}</td>
                             <td>{{ $formatter->format($link['nb_events']) }}</td>
                         </tr>
                     @endforeach
             <div>
                 <h3>{{ __('Mots recherchés') }}</h3>
 
-                <table class="sortable stats-details mt-3">
+                <table class="{!! $tableClasses !!}">
                     <thead>
                     <tr>
                         <th>{{ __('Requêtes') }}</th>
-                        <th class="sorttable_sorted_descending">{{ __('Nombre') }}</th>
+                        <th class="sorttable_sorted_reverse">{{ __('Nombre') }}</th>
                     </tr>
                     </thead>
                     <tbody>
             <div>
                 <h3>{{ __('Origine des visiteurs') }}</h3>
 
-                <table class="sortable stats-details mt-3">
+                <table class="{!! $tableClasses !!}">
                     <thead>
                     <tr>
                         <th>{{ __('Pays') }}</th>
-                        <th class="sorttable_sorted_descending">{{ __('Nombre de visites') }}</th>
+                        <th class="sorttable_sorted_reverse">{{ __('Nombre de visites') }}</th>
                     </tr>
                     </thead>
                     <tbody>
     </div>
 @endcan
 
-{{--================== SCRIPTS ==================--}}
-
-{{-- Date Range picker setup: https://sensortower.github.io/daterangepicker/docs --}}
-<script>
-    moment.locale('{{ $locale }}');
-    @if ($locale === 'en')
-    moment.updateLocale('{{ $locale }}', {
-        longDateFormat: {
-            // Date range picker uses the 'L' format for displaying dates
-            L: 'DD/MM/YYYY', // We don't like the default, backwards US date format
-        }
-    });
-    @endif
+@push('after_scripts')
+    {{--================== SCRIPTS ==================--}}
+
+    {{-- Date Range picker setup: https://sensortower.github.io/daterangepicker/docs --}}
+    <script>
+        moment.locale('{{ $locale }}');
+        @if ($locale === 'en')
+        moment.updateLocale('{{ $locale }}', {
+            longDateFormat: {
+                // Date range picker uses the 'L' format for displaying dates
+                L: 'DD/MM/YYYY', // We don't like the default, backwards US date format
+            }
+        });
+        @endif
 
-    $('[data-daterangepicker]').daterangepicker({
-        callback: function (startDate, endDate, period) {
-            showStatsLoader();
-            let range = startDate.format('YYYY-MM-DD') + ',' + endDate.format('YYYY-MM-DD');
-            let segmentation = document.querySelector('.daterangepicker [name="segmentation"]')?.value || false;
-            let periodOverride = segmentation ? `/${segmentation}` : '';
-            location.href = `{{ $base_URL }}/${range}${periodOverride}`;
-        },
-        forceUpdate: false,
-        minDate: '{{ $fluidbook->created_at->isoFormat('YYYY-MM-DD') }}', // Creation date of the Fluidbook
-        startDate: '{{ $start_date }}',
-        endDate: '{{ $end_date }}',
-
-        // TODO: add translations - check https://github.com/sensortower/daterangepicker/issues/42
-
-        locale: {
-            applyButtonTitle: '{{ __('Appliquer')  }}',
-            cancelButtonTitle: '{{ __('Annuler')  }}',
-            startLabel: '{{ __('Début')  }}',
-            endLabel: '{{ __('Fin')  }}',
-        }
-
-    });
-
-    {{-- Inject the segmentation (period) dropdown into the date picker --}}
-    let picker = document.querySelector('.daterangepicker');
-
-    let segmentOptions = '<option value="">{{ __('Affichage').__(' : ').__('Automatique') }}</option>';
-    @foreach ($period_map as $period_key => $period_title)
-        segmentOptions += '<option value="{{ $period_key }}">{{ __('Affichage').__(' : ').$period_title['periodic'] }}</option>'
-    @endforeach
-
-    let segmentation = document.createElement('select');
-    segmentation.name = 'segmentation';
-    segmentation.innerHTML = segmentOptions;
-    picker.querySelector('form').prepend(segmentation);
-
-    // Add arrow between range inputs
-    let rangeInputs = picker.querySelector('.custom-range-inputs');
-    let arrow = document.createElement('span');
-    arrow.className = 'arrow-right';
-    rangeInputs.insertBefore(arrow, rangeInputs.children[1]); // Insert in 2nd position
-</script>
-
-{{--============================================================================================================--}}
-
-{{-- Charting library --}}
-<script>
-    //=== Chart Setup
-    const labels = {!! json_encode(array_keys($tooltip_labels)) !!};
-    const tooltip_labels = {!! json_encode($tooltip_labels) !!};
-    const data = {
-        labels: labels,
-        datasets: {!! $chart_datasets !!}
-    };
-
-    let maxvisits = 0;
-    $.each(data.datasets[1].data, function (k, v) {
-        if (v === null) {
-            return;
-        }
-        maxvisits = Math.max(maxvisits, v);
-    });
-
-    //=== Plugins
-    //== Offset Bars Plugin: creates a small offset between stacked bars
-    const offsetBars = {
-        id: 'offsetBars',
-        beforeDatasetsDraw(chart, args, options) {
-
-            // Create an offset between the stacked bars
-            chart.config.data.datasets.forEach(function (dataset, datasetIndex) {
-
-                const bar_offset_percentage = dataset.bar_offset || 0;
-
-                // Go through each data point (bar) and apply the horizontal positioning offset
-                chart.getDatasetMeta(datasetIndex).data.forEach(function (dataPoint, index) {
-
-                    let offset = Math.round(dataPoint.width * bar_offset_percentage / 100);
-
-                    // Make sure offset amount isn't too small
-                    if (offset > 0 && offset < 2) {
-                        offset = 2;
-                    } else if (offset < 0 && offset > -2) {
-                        offset = -2;
-                    }
+        $(function () {
+            $('[data-daterangepicker]').daterangepicker({
+                callback: function (startDate, endDate, period) {
+                    let range = startDate.format('YYYY-MM-DD') + ',' + endDate.format('YYYY-MM-DD');
+                    let segmentation = document.querySelector('.daterangepicker [name="segmentation"]')?.value || false;
+                    let periodOverride = segmentation ? `/${segmentation}` : '';
+                    location.href = `{{ $base_URL }}/${range}${periodOverride}`;
+                },
+                forceUpdate: false,
+                minDate: '{{ $fluidbook->created_at->isoFormat('YYYY-MM-DD') }}', // Creation date of the Fluidbook
+                startDate: '{{ $start_date }}',
+                endDate: '{{ $end_date }}',
+
+                // TODO: add translations - check https://github.com/sensortower/daterangepicker/issues/42
+
+                locale: {
+                    applyButtonTitle: '{{ __('Appliquer')  }}',
+                    cancelButtonTitle: '{{ __('Annuler')  }}',
+                    startLabel: '{{ __('Début')  }}',
+                    endLabel: '{{ __('Fin')  }}',
+                }
 
-                    dataPoint.x = chart.scales.x.getPixelForValue(index) + offset;
-                });
             });
-        }
-    };
-
-    //=== Chart Configuration
-    const config = {
-        type: 'bar',
-        data: data,
-        options: {
-            responsive: true,
-            maintainAspectRatio: false,
-            maxBarThickness: 50, // Prevent bars being ridiculously wide when there isn't much data
-            scales: {
-                x: {stacked: true},
-                y: {
-                    type: 'linear', // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance
-                    position: 'left',
-                    stacked: false,
-                },
-                y2: {
-                    type: 'linear', // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance
-                    position: 'right',
-                    reverse: false,
-                    ticks: {
-                        color: '#9bc200',
-                    },
-                    grid: {
-                        drawOnChartArea: false // only want the grid lines for one axis to show up
-                    },
-                    max: maxvisits * 3,
-                    stacked: false,
-                },
-            },
-            plugins: {
-                tooltip: {
-                    mode: 'index',
-                    position: 'nearest',
-                    callbacks: {
-                        title: function (context) {
-                            return tooltip_labels[context[0].label];
-                        }
-                    },
+
+
+            {{-- Inject the segmentation (period) dropdown into the date picker --}}
+            let picker = $('.daterangepicker');
+
+            let segmentOptions = '<option value="">{{ __('Affichage').__(' : ').__('Automatique') }}</option>';
+            @foreach ($period_map as $period_key => $period_title)
+                segmentOptions += '<option value="{{ $period_key }}">{{ __('Affichage').__(' : ').$period_title['periodic'] }}</option>'
+            @endforeach
+
+            let segmentation = document.createElement('select');
+            segmentation.name = 'segmentation';
+            segmentation.innerHTML = segmentOptions;
+            picker.find('form').prepend(segmentation);
+
+            // Add arrow between range inputs
+            let rangeInputs = $(picker).find('.custom-range-inputs');
+            let arrow = document.createElement('span');
+            arrow.className = 'arrow-right';
+            rangeInputs.insertBefore(arrow, rangeInputs.children[1]); // Insert in 2nd position
+
+            {{--============================================================================================================--}}
+
+            {{-- Charting library --}}
+            //=== Chart Setup
+            const labels = {!! json_encode(array_keys($tooltip_labels)) !!};
+            const tooltip_labels = {!! json_encode($tooltip_labels) !!};
+            const data = {
+                labels: labels,
+                datasets: {!! $chart_datasets !!}
+            };
+
+            let maxvisits = 0;
+            $.each(data.datasets[1].data, function (k, v) {
+                if (v === null) {
+                    return;
                 }
-            }
-        },
-        plugins: [offsetBars]
-    };
+                maxvisits = Math.max(maxvisits, v);
+            });
+
+            //=== Plugins
+            //== Offset Bars Plugin: creates a small offset between stacked bars
+            const offsetBars = {
+                id: 'offsetBars',
+                beforeDatasetsDraw(chart, args, options) {
 
-    //=== Render Chart
-    const statsChart = new Chart(
-        document.getElementById('stats_chart'),
-        config
-    );
+                    // Create an offset between the stacked bars
+                    chart.config.data.datasets.forEach(function (dataset, datasetIndex) {
+
+                        const bar_offset_percentage = dataset.bar_offset || 0;
+
+                        // Go through each data point (bar) and apply the horizontal positioning offset
+                        chart.getDatasetMeta(datasetIndex).data.forEach(function (dataPoint, index) {
+
+                            let offset = Math.round(dataPoint.width * bar_offset_percentage / 100);
+
+                            // Make sure offset amount isn't too small
+                            if (offset > 0 && offset < 2) {
+                                offset = 2;
+                            } else if (offset < 0 && offset > -2) {
+                                offset = -2;
+                            }
+
+                            dataPoint.x = chart.scales.x.getPixelForValue(index) + offset;
+                        });
+                    });
+                }
+            };
+
+            //=== Chart Configuration
+            const config = {
+                type: 'bar',
+                data: data,
+                options: {
+                    responsive: true,
+                    maintainAspectRatio: false,
+                    maxBarThickness: 50, // Prevent bars being ridiculously wide when there isn't much data
+                    scales: {
+                        x: {stacked: true},
+                        y: {
+                            type: 'linear', // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance
+                            position: 'left',
+                            stacked: false,
+                        },
+                        y2: {
+                            type: 'linear', // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance
+                            position: 'right',
+                            reverse: false,
+                            ticks: {
+                                color: '#9bc200',
+                            },
+                            grid: {
+                                drawOnChartArea: false // only want the grid lines for one axis to show up
+                            },
+                            max: maxvisits * 3,
+                            stacked: false,
+                        },
+                    },
+                    plugins: {
+                        tooltip: {
+                            mode: 'index',
+                            position: 'nearest',
+                            callbacks: {
+                                title: function (context) {
+                                    return tooltip_labels[context[0].label];
+                                }
+                            },
+                        }
+                    }
+                },
+                plugins: [offsetBars]
+            };
 
-</script>
+            //=== Render Chart
+            const statsChart = new Chart(
+                document.getElementById('stats_chart'),
+                config
+            );
+        });
 
-{{--============================================================================================================--}}
 
-{{-- Simple Table Sorter --}}
-<script type="text/javascript" src="{{ asset('packages/sorttable/sorttable.js') }}"></script>
-{{-- This script works on any tables with the "sortable" class. There's no extra setup needed here. --}}
+    </script>
+@endpush