diff --git a/src/app/Http/Controllers/API/V4/Admin/StatsController.php b/src/app/Http/Controllers/API/V4/Admin/StatsController.php --- a/src/app/Http/Controllers/API/V4/Admin/StatsController.php +++ b/src/app/Http/Controllers/API/V4/Admin/StatsController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\API\V4\Admin; +use App\Providers\PaymentProvider; use App\User; use Carbon\Carbon; use Illuminate\Http\Request; @@ -15,6 +16,7 @@ public const COLOR_RED_DARK = '#a71d2a'; public const COLOR_BLUE = '#4da3ff'; // '#007bff' public const COLOR_BLUE_DARK = '#0056b3'; + public const COLOR_ORANGE = '#f1a539'; /** * Fetch chart data @@ -41,6 +43,122 @@ } /** + * Get discounts chart + */ + protected function chartDiscounts(): array + { + // TODO: Exclude deleted account wallets + $discounts = DB::table('wallets') + ->join('discounts', 'discounts.id', '=', 'wallets.discount_id') + ->selectRaw("discount, count(discount_id) as cnt") + ->groupBy('discounts.discount') + ->get() + ->pluck('cnt', 'discount') + ->all(); + + $labels = array_keys($discounts); + $discounts = array_values($discounts); + + // $labels = [10, 25, 30, 100]; + // $discounts = [100, 120, 30, 50]; + + $labels = array_map(function ($item) { + return $item . '%'; + }, $labels); + + // See https://frappe.io/charts/docs for format/options description + + return [ + 'title' => 'Discounts', + 'type' => 'donut', + 'colors' => [ + self::COLOR_BLUE, + self::COLOR_BLUE_DARK, + self::COLOR_GREEN, + self::COLOR_GREEN_DARK, + self::COLOR_ORANGE, + self::COLOR_RED, + self::COLOR_RED_DARK + ], + 'maxSlices' => 8, + 'tooltipOptions' => [], // does not work without it (https://github.com/frappe/charts/issues/314) + 'data' => [ + 'labels' => $labels, + 'datasets' => [ + [ + 'values' => $discounts + ] + ] + ] + ]; + } + + /** + * Get income chart + */ + protected function chartIncome(): array + { + $weeks = 8; + $start = Carbon::now(); + $labels = []; + + while ($weeks > 0) { + $labels[] = $start->format('Y-W'); + $start->subWeeks(1); + $weeks--; + } + + $labels = array_reverse($labels); + $start->startOfWeek(Carbon::MONDAY); + + $payments = DB::table('payments') + ->selectRaw("concat(year(updated_at), '-', week(updated_at, 3)) as period, sum(amount) as amount") + ->where('updated_at', '>=', $start->toDateString()) + ->where('status', PaymentProvider::STATUS_PAID) + ->whereIn('type', [PaymentProvider::TYPE_ONEOFF, PaymentProvider::TYPE_RECURRING]) + ->groupByRaw('1') + ->get() + ->pluck('amount', 'period') + ->map(function ($amount) { + return $amount / 100; + }); + + // TODO: exclude refunds/chargebacks + + $empty = array_fill_keys($labels, 0); + $payments = array_values(array_merge($empty, $payments->all())); + + // $payments = [1000, 1200.25, 3000, 1897.50, 2000, 1900, 2134, 3330]; + + // See https://frappe.io/charts/docs for format/options description + + return [ + 'title' => 'Income in CHF - last 8 weeks', + 'type' => 'bar', + 'colors' => [self::COLOR_BLUE], + 'axisOptions' => [ + 'xIsSeries' => true, + ], + 'data' => [ + 'labels' => $labels, + 'datasets' => [ + [ + // 'name' => 'Payments', + 'values' => $payments + ] + ], + 'yMarkers' => [ + [ + 'label' => 'average', + 'value' => collect($payments)->avg(), + 'options' => [ 'labelPos' => 'left' ] // default: 'right' + ] + ] + ] + ]; + } + + /** * Get created/deleted users chart */ protected function chartUsers(): array @@ -71,11 +189,11 @@ ->get(); $empty = array_fill_keys($labels, 0); - $created = array_merge($empty, $created->pluck('cnt', 'period')->all()); - $deleted = array_merge($empty, $deleted->pluck('cnt', 'period')->all()); + $created = array_values(array_merge($empty, $created->pluck('cnt', 'period')->all())); + $deleted = array_values(array_merge($empty, $deleted->pluck('cnt', 'period')->all())); - //$created = [5, 2, 4, 2, 0, 5, 2, 4]; - //$deleted = [1, 2, 3, 1, 2, 1, 2, 3]; + // $created = [5, 2, 4, 2, 0, 5, 2, 4]; + // $deleted = [1, 2, 3, 1, 2, 1, 2, 3]; // See https://frappe.io/charts/docs for format/options description @@ -83,6 +201,9 @@ 'title' => 'Users - last 8 weeks', // 'type' => 'axis-mixed', 'colors' => [self::COLOR_GREEN, self::COLOR_RED], + 'axisOptions' => [ + 'xIsSeries' => true, + ], 'data' => [ 'labels' => $labels, 'datasets' => [ @@ -96,6 +217,89 @@ 'chartType' => 'line', 'values' => $deleted ] + ], + 'yMarkers' => [ + [ + 'label' => 'average', + 'value' => collect($created)->avg(), + 'options' => [ 'labelPos' => 'left' ] // default: 'right' + ] + ] + ] + ]; + } + + /** + * Get all users chart + */ + protected function chartUsersAll(): array + { + $weeks = 54; + $start = Carbon::now(); + $labels = []; + + while ($weeks > 0) { + $labels[] = $start->format('Y-W'); + $start->subWeeks(1); + $weeks--; + } + + $labels = array_reverse($labels); + $start->startOfWeek(Carbon::MONDAY); + + $created = DB::table('users') + ->selectRaw("concat(year(created_at), '-', week(created_at, 3)) as period, count(*) as cnt") + ->where('created_at', '>=', $start->toDateString()) + ->groupByRaw('1') + ->get(); + + $deleted = DB::table('users') + ->selectRaw("concat(year(deleted_at), '-', week(deleted_at, 3)) as period, count(*) as cnt") + ->where('deleted_at', '>=', $start->toDateString()) + ->groupByRaw('1') + ->get(); + + $count = DB::table('users')->whereNull('deleted_at')->count(); + + $empty = array_fill_keys($labels, 0); + $created = array_merge($empty, $created->pluck('cnt', 'period')->all()); + $deleted = array_merge($empty, $deleted->pluck('cnt', 'period')->all()); + $all = []; + + foreach (array_reverse($labels) as $label) { + $diff = $created[$label] - $deleted[$label]; + $all[] = $count - $created[$label]; + $count -= $diff; + } + + $all = array_reverse($all); + + // $start = 3000; + // for ($i = 0; $i < count($labels); $i++) { + // $all[$i] = $start + $i * 15; + // } + + // See https://frappe.io/charts/docs for format/options description + + return [ + 'title' => 'All Users - last year', + 'type' => 'line', + 'colors' => [self::COLOR_GREEN], + 'axisOptions' => [ + 'xIsSeries' => true, + 'xAxisMode' => 'tick', + ], + 'lineOptions' => [ + 'hideDots' => true, + 'regionFill' => true, + ], + 'data' => [ + 'labels' => $labels, + 'datasets' => [ + [ + // 'name' => 'Existing', + 'values' => $all + ] ] ] ]; diff --git a/src/resources/themes/charts.scss b/src/resources/themes/charts.scss --- a/src/resources/themes/charts.scss +++ b/src/resources/themes/charts.scss @@ -14,6 +14,28 @@ border: 1px solid lightgrey; border-radius: 0.5em; padding-top: 0.5em; + margin: 0.5em; min-height: 250px; + text-align: center; + + & > span { + line-height: 240px; + } } -} \ No newline at end of file +} + +@include media-breakpoint-up(lg) { + #stats-container { + display: flex; + flex-wrap: wrap; + align-content: baseline; + + & > div { + width: calc(50% - 1em); + + svg { + width: 100%; + } + } + } +} diff --git a/src/resources/vue/Admin/Stats.vue b/src/resources/vue/Admin/Stats.vue --- a/src/resources/vue/Admin/Stats.vue +++ b/src/resources/vue/Admin/Stats.vue @@ -13,7 +13,7 @@ } }, mounted() { - this.loadChart('users') + ['users', 'users-all', 'income', 'discounts'].forEach(chart => this.loadChart(chart)) }, methods: { drawChart(name, data) { @@ -35,6 +35,11 @@ this.$root.removeLoader(chart) this.drawChart(name, response.data) }) + .catch(error => { + console.error(error) + this.$root.removeLoader(chart) + chart.append($('').text('Failed to load data.')) + }) } } }