Selaa lähdekoodia

fix: dashboard

Muhammad Iqbal Afandi 3 vuotta sitten
vanhempi
commit
6f8c411284

+ 65
- 1
app/Http/Controllers/DashboardController.php Näytä tiedosto

@@ -2,6 +2,14 @@
2 2
 
3 3
 namespace App\Http\Controllers;
4 4
 
5
+use App\Models\Member;
6
+use App\Models\Mutation;
7
+use App\Models\TopUp;
8
+use App\Models\TypeMember;
9
+use App\Models\TypeVehicle;
10
+use App\Services\MutationService;
11
+use App\Services\TopUpService;
12
+use Carbon\Carbon;
5 13
 use Illuminate\Http\Request;
6 14
 
7 15
 class DashboardController extends Controller
@@ -14,6 +22,62 @@ class DashboardController extends Controller
14 22
      */
15 23
     public function __invoke(Request $request)
16 24
     {
17
-        return inertia('home/Index.vue');
25
+        $member = Member::get();
26
+
27
+        $typeMember = TypeMember::get();
28
+
29
+        $typeVehicle = TypeVehicle::get();
30
+
31
+        $mutation = Mutation::whereYear('created_at', date('Y'))
32
+            ->get()
33
+            ->groupBy([
34
+                fn($mutation) => $mutation->type,
35
+                fn($mutation) => Carbon::parse($mutation->getRawOriginal('created_at'))->format('M'),
36
+            ]);
37
+
38
+        $topUp = TopUp::get()->groupBy('member_id');
39
+        return inertia('home/Index.vue', [
40
+            'cardStatistics' => [
41
+                //  [
42
+                //     'title' => ...,
43
+                //     'icon' => ...',
44
+                //     'amount' => ...,
45
+                //     'amountLabel' => ...,
46
+                //     'value' => ...,
47
+                // ],
48
+                [
49
+                    'title' => __('words.member'),
50
+                    'icon' => 'pi pi-id-card',
51
+                    'amount' => $member->count(),
52
+                    'amountLabel' => __('words.total'),
53
+                ],
54
+                [
55
+                    'title' => __('words.type_member'),
56
+                    'icon' => 'pi pi-id-card',
57
+                    'amount' => $typeMember->count(),
58
+                    'amountLabel' => __('words.total'),
59
+                ],
60
+                [
61
+                    'title' => __('words.type_vehicle'),
62
+                    'icon' => 'pi pi-car',
63
+                    'amount' => $typeVehicle->count(),
64
+                    'amountLabel' => __('words.total'),
65
+                ],
66
+            ],
67
+            'barStatistics' => [
68
+                [
69
+                    'title' => __('words.mutation_statistic'),
70
+                    'description' => __('words.per_year') . ' ' . date('Y'),
71
+                    'data' => (new MutationService)->statistic($mutation),
72
+                ],
73
+            ],
74
+            'barHorizontalStatistics' => [
75
+                [
76
+                    'title' => __('words.top_up_rank'),
77
+                    'description' => __('words.top_up_number_rank', ['number' => 5]),
78
+                    'data' => (new TopUpService)->topUpRank($topUp),
79
+                ],
80
+            ],
81
+        ]);
18 82
     }
19 83
 }

+ 2
- 2
app/Services/MutationService.php Näytä tiedosto

@@ -63,9 +63,9 @@ class MutationService extends CurrencyFormatService
63 63
         return $collections->transform(fn($collection) => $collection->sum(fn($collect) => $collect->getRawOriginal('amount')));
64 64
     }
65 65
 
66
-    public function statisticData(SupportCollection $collections, int $take = -1)
66
+    public function statistic(SupportCollection $collections)
67 67
     {
68
-        $collections = $collections->take($take);
68
+        $collections = $collections;
69 69
         $collections->transform(fn($collections) => $this->totalPerMonth($collections));
70 70
         return $collections;
71 71
     }

+ 21
- 0
app/Services/TopUpService.php Näytä tiedosto

@@ -0,0 +1,21 @@
1
+<?php
2
+
3
+namespace App\Services;
4
+
5
+use Illuminate\Database\Eloquent\Collection as EloquentCollection;
6
+
7
+class TopUpService extends CurrencyFormatService
8
+{
9
+    public function topUpRank(EloquentCollection $collections, int $take = 5)
10
+    {
11
+        return $collections
12
+            ->transform(fn($collects) => [[
13
+                'name' => $collects->first()->member->name,
14
+                'phone' => $collects->first()->member->phone,
15
+                'amount' => $collects->sum(fn($collect) => $collect->getRawOriginal('amount')),
16
+            ]])
17
+            ->sortByDesc('amount')
18
+            ->take($take)
19
+            ->flatten(1);
20
+    }
21
+}

+ 5
- 0
lang/en/words.php Näytä tiedosto

@@ -31,5 +31,10 @@ return [
31 31
     'per_year' => 'Per Year',
32 32
     'top_customer' => 'Top customer',
33 33
     'top_number_customer' => 'Top :number Customer',
34
+    'member' => 'Member',
35
+    'type_member' => 'Type Member',
36
+    'type_vehicle' => 'Type Kendaraan',
37
+    'top_up_rank' => 'Top Up',
38
+    'top_up_number_rank' => 'Top Up :number Rank',
34 39
 
35 40
 ];

+ 5
- 0
lang/id/words.php Näytä tiedosto

@@ -31,5 +31,10 @@ return [
31 31
     'per_year' => 'Pertahun',
32 32
     'top_customer' => 'Top Pelanggan',
33 33
     'top_number_customer' => 'Top :number Pelanggan',
34
+    'member' => 'Member',
35
+    'type_member' => 'Jenis Member',
36
+    'type_vehicle' => 'Jenis Kendaraan',
37
+    'top_up_rank' => 'Top Up',
38
+    'top_up_number_rank' => 'Peringkat :number Top Up',
34 39
 
35 40
 ];

+ 17828
- 128
public/js/resources_js_pages_home_Index_vue.js
File diff suppressed because it is too large
Näytä tiedosto


+ 29
- 0
public/js/resources_js_pages_home_TableHeader_js.js Näytä tiedosto

@@ -0,0 +1,29 @@
1
+"use strict";
2
+(self["webpackChunk"] = self["webpackChunk"] || []).push([["resources_js_pages_home_TableHeader_js"],{
3
+
4
+/***/ "./resources/js/pages/home/TableHeader.js":
5
+/*!************************************************!*\
6
+  !*** ./resources/js/pages/home/TableHeader.js ***!
7
+  \************************************************/
8
+/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
9
+
10
+__webpack_require__.r(__webpack_exports__);
11
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
12
+/* harmony export */   "TypeMemberTable": () => (/* binding */ TypeMemberTable),
13
+/* harmony export */   "TypeVehicleTable": () => (/* binding */ TypeVehicleTable)
14
+/* harmony export */ });
15
+var TypeVehicleTable = [{
16
+  field: 'type',
17
+  header: 'Jenis Kendaraan'
18
+}];
19
+var TypeMemberTable = [{
20
+  field: 'type',
21
+  header: 'Jenis Member'
22
+}, {
23
+  field: 'price',
24
+  header: 'Tarif Member'
25
+}];
26
+
27
+/***/ })
28
+
29
+}]);

+ 45
- 0
public/js/resources_js_pages_home_TableHeader_vue.js Näytä tiedosto

@@ -0,0 +1,45 @@
1
+"use strict";
2
+(self["webpackChunk"] = self["webpackChunk"] || []).push([["resources_js_pages_home_TableHeader_vue"],{
3
+
4
+/***/ "./node_modules/vue-loader/dist/exportHelper.js":
5
+/*!******************************************************!*\
6
+  !*** ./node_modules/vue-loader/dist/exportHelper.js ***!
7
+  \******************************************************/
8
+/***/ ((__unused_webpack_module, exports) => {
9
+
10
+
11
+Object.defineProperty(exports, "__esModule", ({ value: true }));
12
+// runtime helper for setting properties on components
13
+// in a tree-shakable way
14
+exports["default"] = (sfc, props) => {
15
+    const target = sfc.__vccOpts || sfc;
16
+    for (const [key, val] of props) {
17
+        target[key] = val;
18
+    }
19
+    return target;
20
+};
21
+
22
+
23
+/***/ }),
24
+
25
+/***/ "./resources/js/pages/home/TableHeader.vue":
26
+/*!*************************************************!*\
27
+  !*** ./resources/js/pages/home/TableHeader.vue ***!
28
+  \*************************************************/
29
+/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
30
+
31
+__webpack_require__.r(__webpack_exports__);
32
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
33
+/* harmony export */   "default": () => (__WEBPACK_DEFAULT_EXPORT__)
34
+/* harmony export */ });
35
+/* harmony import */ var _home_dijitalcode_Projects_parkirin_node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./node_modules/vue-loader/dist/exportHelper.js */ "./node_modules/vue-loader/dist/exportHelper.js");
36
+const script = {}
37
+
38
+;
39
+const __exports__ = /*#__PURE__*/(0,_home_dijitalcode_Projects_parkirin_node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_0__["default"])(script, [['__file',"resources/js/pages/home/TableHeader.vue"]])
40
+
41
+/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (__exports__);
42
+
43
+/***/ })
44
+
45
+}]);

+ 1
- 1
public/js/vue.js Näytä tiedosto

@@ -58388,7 +58388,7 @@ module.exports = JSON.parse('{"name":"axios","version":"0.21.4","description":"P
58388 58388
 /******/ 		// This function allow to reference async chunks
58389 58389
 /******/ 		__webpack_require__.u = (chunkId) => {
58390 58390
 /******/ 			// return url for filenames based on template
58391
-/******/ 			return "js/" + chunkId + ".js?id=" + {"node_modules_chart_js_auto_auto_esm_js":"10c6b388645ceb22","resources_js_pages_auth_ForgotPassword_vue":"5b9f0529bda25a9b","resources_js_pages_auth_Login_vue":"61b9c9ae1ae9da32","resources_js_pages_auth_ResetPassword_vue":"b091193a1e114ce8","resources_js_pages_auth_VerifyEmail_vue":"d9853eae0a0235f2","resources_js_pages_expense_Create_vue":"310e7d98ea6b356a","resources_js_pages_expense_Index_vue":"bc7ec2fb070cf568","resources_js_pages_expense_Show_vue":"877a19596ee241b4","resources_js_pages_expense_TableHeader_js":"eed3f0613f167cfd","resources_js_pages_home_Index_vue":"4c4f4fe595ce63c4","resources_js_pages_member_Create_vue":"838296974c6b4913","resources_js_pages_member_Edit_vue":"bc2c701451f6a653","resources_js_pages_member_Index_vue":"caaaba41d56c9f67","resources_js_pages_member_TableHeader_js":"51dbf053f7ddd45c","resources_js_pages_mutation_Report_vue":"6e9122d512297940","resources_js_pages_mutation_TableHeader_js":"7822e888aa3c52fc","resources_js_pages_topup_Create_vue":"958203b0038681b5","resources_js_pages_topup_Index_vue":"ccfe12c579a19810","resources_js_pages_topup_Show_vue":"4d5dea1719d4048e","resources_js_pages_topup_TableHeader_js":"601b7c0a855ce64e","resources_js_pages_typemember_Create_vue":"5bba3a97069a1889","resources_js_pages_typemember_Edit_vue":"8ab79ae9735ce84f","resources_js_pages_typemember_Index_vue":"37e8f6153840325d","resources_js_pages_typemember_TableHeader_js":"ac1d31a59f8d464e","resources_js_pages_typevehicle_Create_vue":"4799ba8b5384d9a3","resources_js_pages_typevehicle_Edit_vue":"1ec4ba3e7994e2bc","resources_js_pages_typevehicle_Index_vue":"69ab68f9968f9aff","resources_js_pages_typevehicle_TableHeader_js":"a40378918fbe74e1","resources_js_pages_user_Create_vue":"ed7565eb901e854c","resources_js_pages_user_Edit_vue":"4948ac292320388a","resources_js_pages_user_Index_vue":"552950b60f6dbb1b","resources_js_pages_user_Show_vue":"1d1d7702785d1470","resources_js_pages_user_TableHeader_js":"0d87fd422fe40491"}[chunkId] + "";
58391
+/******/ 			return "js/" + chunkId + ".js?id=" + {"node_modules_chart_js_auto_auto_esm_js":"10c6b388645ceb22","resources_js_pages_auth_ForgotPassword_vue":"5b9f0529bda25a9b","resources_js_pages_auth_Login_vue":"61b9c9ae1ae9da32","resources_js_pages_auth_ResetPassword_vue":"b091193a1e114ce8","resources_js_pages_auth_VerifyEmail_vue":"d9853eae0a0235f2","resources_js_pages_expense_Create_vue":"310e7d98ea6b356a","resources_js_pages_expense_Index_vue":"bc7ec2fb070cf568","resources_js_pages_expense_Show_vue":"877a19596ee241b4","resources_js_pages_expense_TableHeader_js":"eed3f0613f167cfd","resources_js_pages_home_Index_vue":"4a180c0ce9438466","resources_js_pages_member_Create_vue":"838296974c6b4913","resources_js_pages_member_Edit_vue":"bc2c701451f6a653","resources_js_pages_member_Index_vue":"caaaba41d56c9f67","resources_js_pages_member_TableHeader_js":"51dbf053f7ddd45c","resources_js_pages_mutation_Report_vue":"6e9122d512297940","resources_js_pages_mutation_TableHeader_js":"7822e888aa3c52fc","resources_js_pages_topup_Create_vue":"958203b0038681b5","resources_js_pages_topup_Index_vue":"ccfe12c579a19810","resources_js_pages_topup_Show_vue":"4d5dea1719d4048e","resources_js_pages_topup_TableHeader_js":"601b7c0a855ce64e","resources_js_pages_typemember_Create_vue":"5bba3a97069a1889","resources_js_pages_typemember_Edit_vue":"8ab79ae9735ce84f","resources_js_pages_typemember_Index_vue":"37e8f6153840325d","resources_js_pages_typemember_TableHeader_js":"ac1d31a59f8d464e","resources_js_pages_typevehicle_Create_vue":"4799ba8b5384d9a3","resources_js_pages_typevehicle_Edit_vue":"1ec4ba3e7994e2bc","resources_js_pages_typevehicle_Index_vue":"69ab68f9968f9aff","resources_js_pages_typevehicle_TableHeader_js":"a40378918fbe74e1","resources_js_pages_user_Create_vue":"ed7565eb901e854c","resources_js_pages_user_Edit_vue":"4948ac292320388a","resources_js_pages_user_Index_vue":"552950b60f6dbb1b","resources_js_pages_user_Show_vue":"1d1d7702785d1470","resources_js_pages_user_TableHeader_js":"0d87fd422fe40491"}[chunkId] + "";
58392 58392
 /******/ 		};
58393 58393
 /******/ 	})();
58394 58394
 /******/ 	

+ 29
- 0
resources/js/components/AppCardStatistic.vue Näytä tiedosto

@@ -0,0 +1,29 @@
1
+<script setup>
2
+defineProps({
3
+  data: {
4
+    type: Object,
5
+    required: true,
6
+  },
7
+})
8
+</script>
9
+
10
+<template>
11
+  <Card class="h-full">
12
+    <template #content>
13
+      <div class="flex justify-content-between mb-3">
14
+        <div>
15
+          <span class="block text-500 font-medium mb-3">{{ data.title }}</span>
16
+          <div v-if="data.value" class="text-900 font-medium text-xl">{{ data.value }}</div>
17
+        </div>
18
+        <div
19
+          class="flex align-items-center justify-content-center bg-orange-100 border-round"
20
+          style="width: 2.5rem; height: 2.5rem"
21
+        >
22
+          <i class="text-orange-500 text-xl" :class="data.icon"></i>
23
+        </div>
24
+      </div>
25
+      <span class="text-green-500 font-medium">{{ data.amount }} </span>
26
+      <span class="text-500"> {{ ' ' + data.amountLabel }}</span>
27
+    </template>
28
+  </Card>
29
+</template>

+ 198
- 1
resources/js/pages/home/Index.vue Näytä tiedosto

@@ -1,9 +1,206 @@
1 1
 <script setup>
2
+import { Head } from '@inertiajs/inertia-vue3'
3
+import { orderBy } from 'lodash'
4
+import AppCardStatistic from '@/components/AppCardStatistic.vue'
2 5
 import AppLayout from '@/layouts/AppLayout.vue'
6
+
7
+defineProps({
8
+  cardStatistics: Object,
9
+  barStatistics: Object,
10
+  barHorizontalStatistics: Object,
11
+})
12
+
13
+const colors = [
14
+  '#349dcf',
15
+  '#00b2da',
16
+  '#00c7dd',
17
+  '#1fdbdb',
18
+  '#57eed3',
19
+  '#88ffc9',
20
+  '#96ed9a',
21
+  '#a8d96c',
22
+  '#bbc242',
23
+  '#cda91d',
24
+]
25
+
26
+const barChart = (chartData) => {
27
+  const colors = ['#349dcf', '#a8d96c']
28
+
29
+  const data = {
30
+    datasets: [],
31
+  }
32
+
33
+  let id = 0
34
+  for (const key in chartData) {
35
+    data.datasets.push({
36
+      label: key,
37
+      backgroundColor: colors[id],
38
+      data: chartData[key],
39
+    })
40
+
41
+    id++
42
+  }
43
+
44
+  return data
45
+}
46
+
47
+const barChartOption = {
48
+  maintainAspectRatio: false,
49
+  datasetFill: false,
50
+}
51
+
52
+const barHorizontalChart = (chartData) => {
53
+  // const data = {
54
+  //   datasets: [],
55
+  // }
56
+
57
+  // let id = 0
58
+  // for (const key in chartData) {
59
+  //   data.datasets.push({
60
+  //     label: key,
61
+  //     backgroundColor: colors[id],
62
+  //     data: chartData[key],
63
+  //   })
64
+
65
+  //   id++
66
+  // }
67
+
68
+  // return data
69
+
70
+  const labels = []
71
+  const data = []
72
+
73
+  for (const chartData of chartData) {
74
+    labels.push([chartData.phone, chartData.name])
75
+    data.push(chartData.amount)
76
+  }
77
+
78
+  return {
79
+    labels: labels,
80
+    datasets: [
81
+      {
82
+        data: data,
83
+        backgroundColor: colors,
84
+      },
85
+    ],
86
+  }
87
+}
88
+
89
+const barHorizontalChartOption = {
90
+  maintainAspectRatio: false,
91
+  datasetFill: false,
92
+  indexAxis: 'y',
93
+  plugins: {
94
+    legend: {
95
+      display: false,
96
+    },
97
+  },
98
+}
99
+
100
+const pieChart = (chartData) => {
101
+  const labels = []
102
+  const data = []
103
+
104
+  for (const key in chartData) {
105
+    labels.push(key)
106
+    data.push(chartData[key])
107
+  }
108
+
109
+  return {
110
+    labels: labels,
111
+    datasets: [
112
+      {
113
+        data: data,
114
+        backgroundColor: colors,
115
+      },
116
+    ],
117
+  }
118
+}
119
+
120
+const pieChartOption = {
121
+  maintainAspectRatio: false,
122
+  datasetFill: false,
123
+}
3 124
 </script>
4 125
 
5 126
 <template>
6 127
   <AppLayout>
7
-    <h1>Web App Parkirin</h1>
128
+    <Head title="Dashboard" />
129
+
130
+    <div class="grid">
131
+      <div class="col-12 flex flex-wrap justify-content-between card-statistic">
132
+        <div v-for="cardStatistic in cardStatistics" class="flex-grow-1">
133
+          <AppCardStatistic :data="cardStatistic" />
134
+        </div>
135
+      </div>
136
+
137
+      <div v-for="barStatistic in barStatistics" class="col-12 md:col-6">
138
+        <Card>
139
+          <template #title>
140
+            <div class="flex flex-column">
141
+              <span>{{ barStatistic.title }}</span>
142
+              <span v-if="barStatistic.description" class="text-base font-normal">{{ barStatistic.description }}</span>
143
+            </div>
144
+          </template>
145
+          <template #content>
146
+            <Chart
147
+              type="bar"
148
+              :width="600"
149
+              :height="300"
150
+              :data="barChart(barStatistic.data)"
151
+              :options="barChartOption"
152
+            />
153
+          </template>
154
+        </Card>
155
+      </div>
156
+
157
+      <div v-for="barHorizontalStatistic in barHorizontalStatistics" class="col-12 md:col-6">
158
+        <Card>
159
+          <template #title>
160
+            <div class="flex flex-column">
161
+              <span>{{ barHorizontalStatistic.title }}</span>
162
+              <span v-if="barHorizontalStatistic.description" class="text-base font-normal">{{
163
+                barHorizontalStatistic.description
164
+              }}</span>
165
+            </div>
166
+          </template>
167
+          <template #content>
168
+            <Chart
169
+              type="bar"
170
+              :width="600"
171
+              :height="300"
172
+              :data="barHorizontalChart(barHorizontalStatistic.data)"
173
+              :options="barHorizontalChartOption"
174
+            />
175
+          </template>
176
+        </Card>
177
+      </div>
178
+
179
+      <!-- <div v-for="pieStatistic in pieStatistics" class="col-12 md:col-6">
180
+        <Card>
181
+          <template #title>
182
+            <div class="flex flex-column">
183
+              <span>{{ pieStatistic.title }}</span>
184
+              <span v-if="pieStatistic.description" class="text-base font-normal">{{ pieStatistic.description }}</span>
185
+            </div>
186
+          </template>
187
+          <template #content>
188
+            <Chart
189
+              type="pie"
190
+              :width="600"
191
+              :height="300"
192
+              :data="pieChart(pieStatistic.data)"
193
+              :options="pieChartOption"
194
+            />
195
+          </template>
196
+        </Card>
197
+      </div> -->
198
+    </div>
8 199
   </AppLayout>
9 200
 </template>
201
+
202
+<style scoped>
203
+.card-statistic {
204
+  gap: 1rem;
205
+}
206
+</style>