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 @@ -24,6 +24,7 @@ 'income', 'users', 'users-all', + 'vouchers', ]; /** @@ -80,31 +81,7 @@ 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 - ] - ] - ] - ]; + return $this->donutChart(\trans('app.chart-discounts'), $labels, $discounts); } /** @@ -177,7 +154,7 @@ // See https://frappe.io/charts/docs for format/options description return [ - 'title' => "Income in {$currency} - last 8 weeks", + 'title' => \trans('app.chart-income', ['currency' => $currency]), 'type' => 'bar', 'colors' => [self::COLOR_BLUE], 'axisOptions' => [ @@ -247,7 +224,7 @@ // See https://frappe.io/charts/docs for format/options description return [ - 'title' => 'Users - last 8 weeks', + 'title' => \trans('app.chart-users'), 'type' => 'bar', // Required to fix https://github.com/frappe/charts/issues/294 'colors' => [self::COLOR_GREEN, self::COLOR_RED], 'axisOptions' => [ @@ -257,19 +234,19 @@ 'labels' => $labels, 'datasets' => [ [ - 'name' => 'Created', + 'name' => \trans('app.chart-created'), 'chartType' => 'bar', 'values' => $created ], [ - 'name' => 'Deleted', + 'name' => \trans('app.chart-deleted'), 'chartType' => 'line', 'values' => $deleted ] ], 'yMarkers' => [ [ - 'label' => sprintf('average = %.1f', $avg), + 'label' => sprintf('%s = %.1f', \trans('app.chart-average'), $avg), 'value' => collect($created)->avg(), 'options' => [ 'labelPos' => 'left' ] // default: 'right' ] @@ -332,7 +309,7 @@ // See https://frappe.io/charts/docs for format/options description return [ - 'title' => 'All Users - last year', + 'title' => \trans('app.chart-allusers'), 'type' => 'line', 'colors' => [self::COLOR_GREEN], 'axisOptions' => [ @@ -356,6 +333,67 @@ } /** + * Get vouchers chart + */ + protected function chartVouchers(): array + { + $vouchers = DB::table('wallets') + ->selectRaw("count(discount_id) as cnt, code") + ->join('discounts', 'discounts.id', '=', 'wallets.discount_id') + ->join('users', 'users.id', '=', 'wallets.user_id') + ->where('discount', '>', 0) + ->whereNotNull('code') + ->whereNull('users.deleted_at') + ->groupBy('discounts.code') + ->havingRaw("count(discount_id) > 0") + ->orderByRaw('1'); + + $addTenantScope = function ($builder, $tenantId) { + return $builder->where('users.tenant_id', $tenantId); + }; + + $vouchers = $this->applyTenantScope($vouchers, $addTenantScope) + ->pluck('cnt', 'code')->all(); + + $labels = array_keys($vouchers); + $vouchers = array_values($vouchers); + + // $labels = ["TEST", "NEW", "OTHER", "US"]; + // $vouchers = [100, 120, 30, 50]; + + return $this->donutChart(\trans('app.chart-vouchers'), $labels, $vouchers); + } + + protected static function donutChart($title, $labels, $data): array + { + // See https://frappe.io/charts/docs for format/options description + + return [ + 'title' => $title, + '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' => $data + ] + ] + ] + ]; + } + + /** * Add tenant scope to the queries when needed * * @param \Illuminate\Database\Query\Builder $query The query diff --git a/src/app/Http/Controllers/API/V4/Reseller/StatsController.php b/src/app/Http/Controllers/API/V4/Reseller/StatsController.php --- a/src/app/Http/Controllers/API/V4/Reseller/StatsController.php +++ b/src/app/Http/Controllers/API/V4/Reseller/StatsController.php @@ -12,6 +12,7 @@ // 'income', 'users', 'users-all', + 'vouchers', ]; /** diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php --- a/src/resources/lang/en/app.php +++ b/src/resources/lang/en/app.php @@ -10,6 +10,15 @@ | The following language lines are used in the application. */ + 'chart-created' => 'Created', + 'chart-deleted' => 'Deleted', + 'chart-average' => 'average', + 'chart-allusers' => 'All Users - last year', + 'chart-discounts' => 'Discounts', + 'chart-vouchers' => 'Vouchers', + 'chart-income' => 'Income in :currency - last 8 weeks', + 'chart-users' => 'Users - last 8 weeks', + 'mandate-delete-success' => 'The auto-payment has been removed.', 'mandate-update-success' => 'The auto-payment has been updated.', 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 @@ -9,7 +9,7 @@ data() { return { charts: {}, - chartTypes: ['users', 'users-all', 'income', 'discounts'] + chartTypes: ['users', 'users-all', 'income', 'discounts', 'vouchers'] } }, mounted() { diff --git a/src/resources/vue/Reseller/Stats.vue b/src/resources/vue/Reseller/Stats.vue --- a/src/resources/vue/Reseller/Stats.vue +++ b/src/resources/vue/Reseller/Stats.vue @@ -9,7 +9,7 @@ mixins: [Stats], data() { return { - chartTypes: ['users', 'users-all', 'discounts'] + chartTypes: ['users', 'users-all', 'discounts', 'vouchers'] } } } diff --git a/src/tests/Browser/Admin/StatsTest.php b/src/tests/Browser/Admin/StatsTest.php --- a/src/tests/Browser/Admin/StatsTest.php +++ b/src/tests/Browser/Admin/StatsTest.php @@ -31,11 +31,12 @@ ->assertSeeIn('@links .link-stats', 'Stats') ->click('@links .link-stats') ->on(new Stats()) - ->assertElementsCount('@container > div', 4) + ->assertElementsCount('@container > div', 5) ->waitForTextIn('@container #chart-users svg .title', 'Users - last 8 weeks') ->waitForTextIn('@container #chart-users-all svg .title', 'All Users - last year') ->waitForTextIn('@container #chart-income svg .title', 'Income in CHF - last 8 weeks') - ->waitForTextIn('@container #chart-discounts svg .title', 'Discounts'); + ->waitForTextIn('@container #chart-discounts svg .title', 'Discounts') + ->waitForTextIn('@container #chart-vouchers svg .title', 'Vouchers'); }); } } diff --git a/src/tests/Browser/Reseller/StatsTest.php b/src/tests/Browser/Reseller/StatsTest.php --- a/src/tests/Browser/Reseller/StatsTest.php +++ b/src/tests/Browser/Reseller/StatsTest.php @@ -42,10 +42,11 @@ ->assertSeeIn('@links .link-stats', 'Stats') ->click('@links .link-stats') ->on(new Stats()) - ->assertElementsCount('@container > div', 3) + ->assertElementsCount('@container > div', 4) ->waitForTextIn('@container #chart-users svg .title', 'Users - last 8 weeks') ->waitForTextIn('@container #chart-users-all svg .title', 'All Users - last year') - ->waitForTextIn('@container #chart-discounts svg .title', 'Discounts'); + ->waitForTextIn('@container #chart-discounts svg .title', 'Discounts') + ->waitForTextIn('@container #chart-vouchers svg .title', 'Vouchers'); }); } } diff --git a/src/tests/Feature/Controller/Admin/DiscountsTest.php b/src/tests/Feature/Controller/Admin/DiscountsTest.php --- a/src/tests/Feature/Controller/Admin/DiscountsTest.php +++ b/src/tests/Feature/Controller/Admin/DiscountsTest.php @@ -42,8 +42,8 @@ $json = $response->json(); - $discount_test = Discount::where('code', 'TEST')->first(); - $discount_free = Discount::where('discount', 100)->first(); + $discount_test = Discount::withObjectTenantContext($user)->where('code', 'TEST')->first(); + $discount_free = Discount::withObjectTenantContext($user)->where('discount', 100)->first(); $this->assertSame(3, $json['count']); $this->assertSame($discount_test->id, $json['list'][0]['id']); diff --git a/src/tests/Feature/Controller/Admin/StatsTest.php b/src/tests/Feature/Controller/Admin/StatsTest.php --- a/src/tests/Feature/Controller/Admin/StatsTest.php +++ b/src/tests/Feature/Controller/Admin/StatsTest.php @@ -4,6 +4,7 @@ use App\Payment; use App\Providers\PaymentProvider; +use Illuminate\Support\Facades\DB; use Tests\TestCase; class StatsTest extends TestCase @@ -17,6 +18,7 @@ self::useAdminUrl(); Payment::truncate(); + DB::table('wallets')->update(['discount_id' => null]); } /** @@ -25,6 +27,7 @@ public function tearDown(): void { Payment::truncate(); + DB::table('wallets')->update(['discount_id' => null]); parent::tearDown(); } @@ -90,6 +93,21 @@ $this->assertSame('All Users - last year', $json['title']); $this->assertCount(54, $json['data']['labels']); $this->assertCount(1, $json['data']['datasets']); + + // 'vouchers' chart + $discount = \App\Discount::withObjectTenantContext($user)->where('code', 'TEST')->first(); + $wallet = $user->wallets->first(); + $wallet->discount()->associate($discount); + $wallet->save(); + + $response = $this->actingAs($admin)->get("api/v4/stats/chart/vouchers"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame('Vouchers', $json['title']); + $this->assertSame(['TEST'], $json['data']['labels']); + $this->assertSame([['values' => [1]]], $json['data']['datasets']); } /** diff --git a/src/tests/Feature/Controller/Reseller/StatsTest.php b/src/tests/Feature/Controller/Reseller/StatsTest.php --- a/src/tests/Feature/Controller/Reseller/StatsTest.php +++ b/src/tests/Feature/Controller/Reseller/StatsTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature\Controller\Reseller; +use Illuminate\Support\Facades\DB; use Tests\TestCase; class StatsTest extends TestCase @@ -13,6 +14,8 @@ { parent::setUp(); self::useResellerUrl(); + + DB::table('wallets')->update(['discount_id' => null]); } /** @@ -20,6 +23,8 @@ */ public function tearDown(): void { + DB::table('wallets')->update(['discount_id' => null]); + parent::tearDown(); } @@ -85,5 +90,20 @@ $this->assertSame('All Users - last year', $json['title']); $this->assertCount(54, $json['data']['labels']); $this->assertCount(1, $json['data']['datasets']); + + // 'vouchers' chart + $discount = \App\Discount::withObjectTenantContext($user)->where('code', 'TEST')->first(); + $wallet = $user->wallets->first(); + $wallet->discount()->associate($discount); + $wallet->save(); + + $response = $this->actingAs($reseller)->get("api/v4/stats/chart/vouchers"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame('Vouchers', $json['title']); + $this->assertSame(['TEST'], $json['data']['labels']); + $this->assertSame([['values' => [1]]], $json['data']['datasets']); } }