浏览代码

feat: master user

父节点
当前提交
437f36a010

+ 190
- 0
app/Http/Controllers/UserController.php 查看文件

@@ -0,0 +1,190 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use App\Http\Controllers\Controller;
6
+use App\Http\Requests\User\ChangePasswordRequest;
7
+use App\Http\Requests\User\StoreUserRequest;
8
+use App\Http\Requests\User\UpdateUserRequest;
9
+use App\Models\Role;
10
+use App\Models\User;
11
+use Illuminate\Support\Facades\Hash;
12
+
13
+class UserController extends Controller
14
+{
15
+    /**
16
+     * Create the controller instance.
17
+     *
18
+     * @return void
19
+     */
20
+    public function __construct()
21
+    {}
22
+
23
+    /**
24
+     * Display a listing of the resource.
25
+     *
26
+     * @return \Inertia\Response
27
+     */
28
+    public function index()
29
+    {
30
+        return inertia('user/Index', [
31
+            'filters' => request()->all('search'),
32
+            'users' => User::filter(request()->only('search'))
33
+                ->latest()
34
+                ->paginate(10)
35
+                ->withQueryString()
36
+                ->through(fn($user) => [
37
+                    'id' => $user->id,
38
+                    'name' => $user->name,
39
+                    'phone' => $user->phone,
40
+                    'email' => $user->email,
41
+                    'role' => $user->role->name,
42
+                    'status' => $user->status,
43
+                ]),
44
+        ]);
45
+    }
46
+
47
+    /**
48
+     * Show the form for creating a new resource.
49
+     *
50
+     * @return \Inertia\Response
51
+     */
52
+    public function create()
53
+    {
54
+        return inertia('user/Create', [
55
+            'roles' => Role::whereNotIn('id', [1])
56
+                ->get()
57
+                ->transform(fn($role) => [
58
+                    'label' => $role->name,
59
+                    'value' => $role->id,
60
+                ]),
61
+        ]);
62
+    }
63
+
64
+    /**
65
+     * Store a newly created resource in storage.
66
+     *
67
+     * @param  \Illuminate\Http\Request  $request
68
+     * @return \Illuminate\Http\Response
69
+     */
70
+    public function store(StoreUserRequest $request)
71
+    {
72
+        User::create($request->validated());
73
+
74
+        return back()->with('success', __('messages.success.store.user'));
75
+    }
76
+
77
+    /**
78
+     * Display the specified resource.
79
+     *
80
+     * @param  User  $user
81
+     * @return \Inertia\Response
82
+     */
83
+    public function show(User $user)
84
+    {
85
+        return inertia('user/Show', [
86
+            'user' => [
87
+                'id' => $user->id,
88
+                'name' => $user->name,
89
+                'phone' => $user->phone,
90
+                'email' => $user->email,
91
+                'role_id' => $user->role_id,
92
+            ],
93
+            'roles' => Role::whereNotIn('id', [1])
94
+                ->get()
95
+                ->transform(fn($role) => [
96
+                    'label' => $role->name,
97
+                    'value' => $role->id,
98
+                ]),
99
+        ]);
100
+    }
101
+
102
+    /**
103
+     * Show the form for editing the specified resource.
104
+     *
105
+     * @param  User  $user
106
+     * @return \Inertia\Response
107
+     */
108
+    public function edit(User $user)
109
+    {
110
+        return inertia('user/Edit', [
111
+            'user' => [
112
+                'id' => $user->id,
113
+                'name' => $user->name,
114
+                'phone' => $user->phone,
115
+                'email' => $user->email,
116
+                'role_id' => $user->role_id,
117
+            ],
118
+            'roles' => Role::whereNotIn('id', [1])
119
+                ->get()
120
+                ->transform(fn($role) => [
121
+                    'label' => $role->name,
122
+                    'value' => $role->id,
123
+                ]),
124
+        ]);
125
+    }
126
+
127
+    /**
128
+     * Update the specified resource in storage.
129
+     *
130
+     * @param  \Illuminate\Http\Request  $request
131
+     * @param  User  $user
132
+     * @return \Illuminate\Http\Response
133
+     */
134
+    public function update(UpdateUserRequest $request, User $user)
135
+    {
136
+        $user->update($request->validated());
137
+
138
+        return back()->with('success', __('messages.success.update.user'));
139
+    }
140
+
141
+    /**
142
+     * Remove the specified resource from storage.
143
+     *
144
+     * @param  User  $user
145
+     * @return \Illuminate\Http\Response
146
+     */
147
+    public function destroy(User $user)
148
+    {
149
+        $user->delete();
150
+
151
+        return to_route('users.index')->with('success', __('messages.success.destroy.user'));
152
+    }
153
+
154
+    /**
155
+     * Block user
156
+     *
157
+     * @param User $user
158
+     * @return \Illuminate\Http\Response
159
+     */
160
+    public function block(User $user)
161
+    {
162
+        $user->status = !$user->getRawOriginal('status');
163
+        $user->update();
164
+
165
+        if ($user->getRawOriginal('status')) {
166
+            $msg = __('messages.user.active_user');
167
+        } else {
168
+            $msg = __('messages.user.no_active_user');
169
+        }
170
+
171
+        return back()->with('success', $msg);
172
+    }
173
+
174
+    /**
175
+     * Change Password
176
+     *
177
+     * @param  \Illuminate\Http\Request  $request
178
+     * @return \Illuminate\Http\Response
179
+     */
180
+    public function changePassword(ChangePasswordRequest $request)
181
+    {
182
+        if (!Hash::check($request->old_password, $request->user()->password)) {
183
+            return back()->with('error', __('messages.error.store.change-password'));
184
+        }
185
+
186
+        $request->user()->update(['password' => bcrypt($request->new_password)]);
187
+
188
+        return back()->with('success', __('messages.success.update.change-password'));
189
+    }
190
+}

+ 32
- 0
app/Http/Requests/User/ChangePasswordRequest.php 查看文件

@@ -0,0 +1,32 @@
1
+<?php
2
+
3
+namespace App\Http\Requests\User;
4
+
5
+use Illuminate\Foundation\Http\FormRequest;
6
+use Illuminate\Validation\Rules\Password;
7
+
8
+class ChangePasswordRequest extends FormRequest
9
+{
10
+    /**
11
+     * Determine if the user is authorized to make this request.
12
+     *
13
+     * @return bool
14
+     */
15
+    public function authorize()
16
+    {
17
+        return true;
18
+    }
19
+
20
+    /**
21
+     * Get the validation rules that apply to the request.
22
+     *
23
+     * @return array
24
+     */
25
+    public function rules()
26
+    {
27
+        return [
28
+            'old_password' => 'required',
29
+            'new_password' => ['required', 'confirmed', Password::defaults()],
30
+        ];
31
+    }
32
+}

+ 33
- 0
app/Http/Requests/User/StoreUserRequest.php 查看文件

@@ -0,0 +1,33 @@
1
+<?php
2
+
3
+namespace App\Http\Requests\User;
4
+
5
+use Illuminate\Foundation\Http\FormRequest;
6
+
7
+class StoreUserRequest extends FormRequest
8
+{
9
+    /**
10
+     * Determine if the user is authorized to make this request.
11
+     *
12
+     * @return bool
13
+     */
14
+    public function authorize()
15
+    {
16
+        return true;
17
+    }
18
+
19
+    /**
20
+     * Get the validation rules that apply to the request.
21
+     *
22
+     * @return array
23
+     */
24
+    public function rules()
25
+    {
26
+        return [
27
+            'name' => 'required|string|max:50',
28
+            'phone' => 'required|numeric|min:12|unique:users,phone',
29
+            'email' => 'required|email|unique:users,email',
30
+            'role_id' => 'required|numeric',
31
+        ];
32
+    }
33
+}

+ 33
- 0
app/Http/Requests/User/UpdateUserRequest.php 查看文件

@@ -0,0 +1,33 @@
1
+<?php
2
+
3
+namespace App\Http\Requests\User;
4
+
5
+use Illuminate\Foundation\Http\FormRequest;
6
+
7
+class UpdateUserRequest extends FormRequest
8
+{
9
+    /**
10
+     * Determine if the user is authorized to make this request.
11
+     *
12
+     * @return bool
13
+     */
14
+    public function authorize()
15
+    {
16
+        return true;
17
+    }
18
+
19
+    /**
20
+     * Get the validation rules that apply to the request.
21
+     *
22
+     * @return array
23
+     */
24
+    public function rules()
25
+    {
26
+        return [
27
+            'name' => 'required|string|max:50',
28
+            'phone' => 'required|numeric|min:12|unique:users,phone,' . $this->user->id,
29
+            'email' => 'required|email|unique:users,email,' . $this->user->id,
30
+            'role_id' => 'required|numeric',
31
+        ];
32
+    }
33
+}

+ 23
- 2
app/Models/User.php 查看文件

@@ -24,9 +24,7 @@ class User extends Authenticatable implements MustVerifyEmail
24 24
         'email',
25 25
         'status',
26 26
         'password',
27
-        'gender_id',
28 27
         'role_id',
29
-        'outlet_id',
30 28
     ];
31 29
 
32 30
     /**
@@ -47,4 +45,27 @@ class User extends Authenticatable implements MustVerifyEmail
47 45
     protected $casts = [
48 46
         'email_verified_at' => 'datetime',
49 47
     ];
48
+
49
+    protected function status(): Attribute
50
+    {
51
+        return Attribute::make(
52
+            get:fn($value) => $value ? __('words.active') : __('words.not_active'),
53
+        );
54
+    }
55
+
56
+    public function role()
57
+    {
58
+        return $this->belongsTo(Role::class);
59
+    }
60
+
61
+    public function scopeFilter($query, array $filters)
62
+    {
63
+        $query->when($filters['search'] ?? null, function ($query, $search) {
64
+            $query->where(function ($query) use ($search) {
65
+                $query->where('name', 'like', '%' . $search . '%')
66
+                    ->orWhere('phone', 'like', '%' . $search . '%')
67
+                    ->orWhere('email', 'like', '%' . $search . '%');
68
+            });
69
+        });
70
+    }
50 71
 }

+ 9
- 8
public/js/resources_js_pages_home_Index_vue.js 查看文件

@@ -769,6 +769,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
769 769
   }]]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("ul", _hoisted_9, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("li", _hoisted_10, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("span", _hoisted_11, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)(_ctx.$page.props.auth.user.name), 1
770 770
   /* TEXT */
771 771
   )]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("li", null, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)($setup["Link"], {
772
+    href: _ctx.route('users.show', _ctx.$page.props.auth.user.id),
772 773
     "class": "p-link layout-topbar-button"
773 774
   }, {
774 775
     "default": (0,vue__WEBPACK_IMPORTED_MODULE_0__.withCtx)(function () {
@@ -777,7 +778,9 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
777 778
     _: 1
778 779
     /* STABLE */
779 780
 
780
-  })]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("li", null, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)($setup["Link"], {
781
+  }, 8
782
+  /* PROPS */
783
+  , ["href"])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("li", null, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)($setup["Link"], {
781 784
     href: _ctx.route('logout'),
782 785
     as: "button",
783 786
     method: "post",
@@ -899,14 +902,12 @@ __webpack_require__.r(__webpack_exports__);
899 902
       component: 'home/Index'
900 903
     }]
901 904
   }, {
902
-    label: 'Menu',
905
+    label: 'Master',
903 906
     items: [{
904
-      label: 'Submenu 1',
905
-      icon: 'pi pi-bookmark',
906
-      items: [{
907
-        label: 'Submenu 1.1',
908
-        icon: 'pi pi-bookmark'
909
-      }]
907
+      label: 'User',
908
+      icon: 'pi pi-user',
909
+      to: '/users',
910
+      component: 'user/Index'
910 911
     }]
911 912
   }],
912 913
   // Supervisor

+ 1874
- 0
public/js/resources_js_pages_user_Create_vue.js
文件差异内容过多而无法显示
查看文件


+ 2280
- 0
public/js/resources_js_pages_user_Edit_vue.js
文件差异内容过多而无法显示
查看文件


+ 6873
- 0
public/js/resources_js_pages_user_Index_vue.js
文件差异内容过多而无法显示
查看文件


+ 1900
- 0
public/js/resources_js_pages_user_Show_vue.js
文件差异内容过多而无法显示
查看文件


+ 33
- 0
public/js/resources_js_pages_user_TableHeader_js.js 查看文件

@@ -0,0 +1,33 @@
1
+"use strict";
2
+(self["webpackChunk"] = self["webpackChunk"] || []).push([["resources_js_pages_user_TableHeader_js"],{
3
+
4
+/***/ "./resources/js/pages/user/TableHeader.js":
5
+/*!************************************************!*\
6
+  !*** ./resources/js/pages/user/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 */   "default": () => (__WEBPACK_DEFAULT_EXPORT__)
13
+/* harmony export */ });
14
+/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ([{
15
+  field: 'name',
16
+  header: 'Nama'
17
+}, {
18
+  field: 'phone',
19
+  header: 'No HP'
20
+}, {
21
+  field: 'email',
22
+  header: 'Email'
23
+}, {
24
+  field: 'role',
25
+  header: 'Hak Akses'
26
+}, {
27
+  field: 'status',
28
+  header: 'Status'
29
+}]);
30
+
31
+/***/ })
32
+
33
+}]);

+ 41
- 1
public/js/vue.js 查看文件

@@ -43810,6 +43810,46 @@ var map = {
43810 43810
 	"./home/Index.vue": [
43811 43811
 		"./resources/js/pages/home/Index.vue",
43812 43812
 		"resources_js_pages_home_Index_vue"
43813
+	],
43814
+	"./user/Create": [
43815
+		"./resources/js/pages/user/Create.vue",
43816
+		"resources_js_pages_user_Create_vue"
43817
+	],
43818
+	"./user/Create.vue": [
43819
+		"./resources/js/pages/user/Create.vue",
43820
+		"resources_js_pages_user_Create_vue"
43821
+	],
43822
+	"./user/Edit": [
43823
+		"./resources/js/pages/user/Edit.vue",
43824
+		"resources_js_pages_user_Edit_vue"
43825
+	],
43826
+	"./user/Edit.vue": [
43827
+		"./resources/js/pages/user/Edit.vue",
43828
+		"resources_js_pages_user_Edit_vue"
43829
+	],
43830
+	"./user/Index": [
43831
+		"./resources/js/pages/user/Index.vue",
43832
+		"resources_js_pages_user_Index_vue"
43833
+	],
43834
+	"./user/Index.vue": [
43835
+		"./resources/js/pages/user/Index.vue",
43836
+		"resources_js_pages_user_Index_vue"
43837
+	],
43838
+	"./user/Show": [
43839
+		"./resources/js/pages/user/Show.vue",
43840
+		"resources_js_pages_user_Show_vue"
43841
+	],
43842
+	"./user/Show.vue": [
43843
+		"./resources/js/pages/user/Show.vue",
43844
+		"resources_js_pages_user_Show_vue"
43845
+	],
43846
+	"./user/TableHeader": [
43847
+		"./resources/js/pages/user/TableHeader.js",
43848
+		"resources_js_pages_user_TableHeader_js"
43849
+	],
43850
+	"./user/TableHeader.js": [
43851
+		"./resources/js/pages/user/TableHeader.js",
43852
+		"resources_js_pages_user_TableHeader_js"
43813 43853
 	]
43814 43854
 };
43815 43855
 function webpackAsyncContext(req) {
@@ -43928,7 +43968,7 @@ module.exports = JSON.parse('{"name":"axios","version":"0.21.4","description":"P
43928 43968
 /******/ 		// This function allow to reference async chunks
43929 43969
 /******/ 		__webpack_require__.u = (chunkId) => {
43930 43970
 /******/ 			// return url for filenames based on template
43931
-/******/ 			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":"a7b9a99e5a331088","resources_js_pages_home_Index_vue":"ac979b71a6556fb9"}[chunkId] + "";
43971
+/******/ 			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":"a7b9a99e5a331088","resources_js_pages_home_Index_vue":"a357d4139a5256e8","resources_js_pages_user_Create_vue":"bd11fb7ccd86dc6e","resources_js_pages_user_Edit_vue":"0b47a99f15339c5f","resources_js_pages_user_Index_vue":"170f708e9932b04b","resources_js_pages_user_Show_vue":"ee8e5d4cb9d97c90","resources_js_pages_user_TableHeader_js":"0d87fd422fe40491"}[chunkId] + "";
43932 43972
 /******/ 		};
43933 43973
 /******/ 	})();
43934 43974
 /******/ 	

+ 85
- 0
resources/js/components/AppAutocompleteBasic.vue 查看文件

@@ -0,0 +1,85 @@
1
+<script setup>
2
+import { computed } from 'vue'
3
+
4
+const props = defineProps({
5
+  field: {
6
+    type: String,
7
+    required: true,
8
+  },
9
+  suggestions: {
10
+    type: Array,
11
+    required: true,
12
+  },
13
+  empty: {
14
+    type: Boolean,
15
+    default: false,
16
+  },
17
+  label: {
18
+    type: String,
19
+    required: true,
20
+  },
21
+  dropdown: {
22
+    type: Boolean,
23
+    default: false,
24
+  },
25
+  placeholder: {
26
+    type: String,
27
+    required: true,
28
+  },
29
+  error: {
30
+    type: String,
31
+    default: null,
32
+  },
33
+  modelValue: null,
34
+})
35
+
36
+defineEmits(['complete', 'itemSelect', 'update:modelValue'])
37
+
38
+const isError = computed(() => (props.error ? true : false))
39
+
40
+const forLabel = computed(() => props.label.toLowerCase().replace(/\s+/g, '-'))
41
+
42
+const ariaDescribedbyLabel = computed(() => props.label.toLowerCase().replace(/\s+/g, '-') + '-help')
43
+</script>
44
+
45
+<template>
46
+  <div class="field">
47
+    <label :for="forLabel">{{ label }}</label>
48
+
49
+    <AutoComplete
50
+      class="w-full"
51
+      inputClass="w-full"
52
+      :model-value="modelValue"
53
+      :aria-describedby="ariaDescribedbyLabel"
54
+      :id="forLabel"
55
+      :class="{ 'p-invalid': isError }"
56
+      :field="field"
57
+      :placeholder="placeholder"
58
+      :suggestions="suggestions"
59
+      :auto-highlight="true"
60
+      :dropdown="dropdown"
61
+      @input="$emit('update:modelValue', $event.target.value)"
62
+      @item-select="$emit('itemSelect', $event)"
63
+      @complete="$emit('complete', $event)"
64
+    >
65
+      <template #item="slotProps">
66
+        <slot name="item" :item="slotProps.item" />
67
+      </template>
68
+    </AutoComplete>
69
+
70
+    <div class="flex flex-column">
71
+      <small
72
+        v-if="error"
73
+        class="mt-1"
74
+        :class="{ 'mb-2': suggestions.length === 0 || (modelValue.length === 0 && empty), 'p-error': isError }"
75
+        :id="ariaDescribedbyLabel"
76
+      >
77
+        {{ error }}
78
+      </small>
79
+
80
+      <small v-if="suggestions.length === 0 || (modelValue.length === 0 && empty)" class="mt-1">
81
+        <slot v-if="empty" name="empty" />
82
+      </small>
83
+    </div>
84
+  </div>
85
+</template>

+ 37
- 0
resources/js/components/AppDialog.vue 查看文件

@@ -0,0 +1,37 @@
1
+<script setup>
2
+defineProps({
3
+  message: {
4
+    type: String,
5
+    default: null,
6
+  },
7
+  header: {
8
+    type: String,
9
+    default: 'Peringatan',
10
+  },
11
+  cancelLabel: {
12
+    type: String,
13
+    default: 'Tidak',
14
+  },
15
+  agreeLabel: {
16
+    type: String,
17
+    default: 'Ya',
18
+  },
19
+})
20
+
21
+defineEmits(['cancel', 'agree'])
22
+</script>
23
+
24
+<template>
25
+  <Dialog :header="header" :style="{ width: '450px' }" :modal="true" :breakpoints="{ '960px': '75vw' }">
26
+    <div class="flex align-items-center justify-content-center">
27
+      <i class="pi pi-exclamation-triangle mr-3" style="font-size: 2rem" />
28
+      <p v-if="message">{{ message }}</p>
29
+    </div>
30
+
31
+    <template #footer>
32
+      <Button :label="cancelLabel" icon="pi pi-times" class="p-button-text" @click="$emit('cancel')" />
33
+
34
+      <Button :label="agreeLabel" icon="pi pi-check" class="p-button-text" @click="$emit('agree')" />
35
+    </template>
36
+  </Dialog>
37
+</template>

+ 95
- 0
resources/js/components/AppDropdown.vue 查看文件

@@ -0,0 +1,95 @@
1
+<script setup>
2
+import { computed } from 'vue'
3
+
4
+const props = defineProps({
5
+  label: {
6
+    type: String,
7
+    required: true,
8
+  },
9
+  optionLabel: {
10
+    type: String,
11
+    default: 'label',
12
+  },
13
+  optionValue: {
14
+    type: String,
15
+    default: 'value',
16
+  },
17
+  optionDisabled: {
18
+    type: String,
19
+    default: 'disabled',
20
+  },
21
+  options: {
22
+    type: Array,
23
+    required: true,
24
+  },
25
+  placeholder: {
26
+    type: String,
27
+    required: true,
28
+  },
29
+  disabled: {
30
+    type: Boolean,
31
+    default: false,
32
+  },
33
+  error: {
34
+    type: String,
35
+    default: null,
36
+  },
37
+  modelValue: null,
38
+})
39
+
40
+defineEmits(['update:modelValue'])
41
+
42
+const isError = computed(() => (props.error ? true : false))
43
+
44
+const forLabel = computed(() => (props.label ? props.label.toLowerCase().replace(/\s+/g, '-') : null))
45
+
46
+const ariaDescribedbyLabel = computed(() =>
47
+  props.label ? props.label.toLowerCase().replace(/\s+/g, '-') + '-help' : null
48
+)
49
+
50
+const selectedDropdownLabel = (value) => {
51
+  const result = props.options.find((option) => option[props.optionValue] == value)
52
+  if (result) {
53
+    return result[props.optionLabel]
54
+  }
55
+}
56
+</script>
57
+
58
+<template>
59
+  <div class="field">
60
+    <label v-if="label" :for="forLabel">{{ label }}</label>
61
+
62
+    <Dropdown
63
+      class="w-full"
64
+      :class="{ 'p-invalid': isError }"
65
+      :id="forLabel"
66
+      :aria-describedby="ariaDescribedbyLabel"
67
+      :option-disabled="optionDisabled"
68
+      :option-label="optionLabel"
69
+      :option-value="optionValue"
70
+      :placeholder="placeholder"
71
+      :options="options"
72
+      :model-value="modelValue"
73
+      :disabled="disabled"
74
+      @change="$emit('update:modelValue', $event.value)"
75
+    >
76
+      <template #value="slotProps">
77
+        <div v-if="slotProps.value">
78
+          {{ selectedDropdownLabel(slotProps.value) }}
79
+        </div>
80
+
81
+        <div v-else>
82
+          {{ slotProps.placeholder }}
83
+        </div>
84
+      </template>
85
+
86
+      <template #option="{ option, index }">
87
+        <slot name="option" :option="option" :index="index" />
88
+      </template>
89
+    </Dropdown>
90
+
91
+    <small v-if="error" :id="ariaDescribedbyLabel" :class="{ 'p-error': isError }">
92
+      {{ error }}
93
+    </small>
94
+  </div>
95
+</template>

+ 56
- 0
resources/js/components/AppEditor.vue 查看文件

@@ -0,0 +1,56 @@
1
+<script setup>
2
+import { computed } from 'vue'
3
+
4
+const props = defineProps({
5
+  label: {
6
+    type: String,
7
+    required: true,
8
+  },
9
+  placeholder: {
10
+    type: String,
11
+    required: true,
12
+  },
13
+  readOnly: {
14
+    type: Boolean,
15
+    required: false,
16
+  },
17
+  error: {
18
+    type: String,
19
+    default: null,
20
+  },
21
+  editorStyle: null,
22
+  modelValue: null,
23
+})
24
+
25
+defineEmits(['update:modelValue'])
26
+
27
+const isError = computed(() => (props.error ? true : false))
28
+
29
+const forLabel = computed(() => (props.label ? props.label.toLowerCase().replace(/\s+/g, '-') : null))
30
+
31
+const ariaDescribedbyLabel = computed(() =>
32
+  props.label ? props.label.toLowerCase().replace(/\s+/g, '-') + '-help' : null
33
+)
34
+</script>
35
+
36
+<template>
37
+  <div class="field">
38
+    <label v-if="label" :for="forLabel">{{ label }}</label>
39
+
40
+    <Editor
41
+      :read-only="readOnly"
42
+      :model-value="modelValue"
43
+      :editor-style="editorStyle"
44
+      :placeholder="placeholder"
45
+      @text-change="$emit('update:modelValue', $event.htmlValue)"
46
+    >
47
+      <template #toolbar>
48
+        <slot name="toolbar" />
49
+      </template>
50
+    </Editor>
51
+
52
+    <small v-if="error" :id="ariaDescribedbyLabel" :class="{ 'p-error': isError }">
53
+      {{ error }}
54
+    </small>
55
+  </div>
56
+</template>

+ 58
- 0
resources/js/components/AppInputText.vue 查看文件

@@ -0,0 +1,58 @@
1
+<script setup>
2
+import { computed } from 'vue'
3
+
4
+const props = defineProps({
5
+  type: {
6
+    type: String,
7
+    default: 'text',
8
+  },
9
+  label: {
10
+    type: String,
11
+    required: true,
12
+  },
13
+  disabled: {
14
+    type: Boolean,
15
+    default: false,
16
+  },
17
+  placeholder: {
18
+    type: String,
19
+    required: true,
20
+  },
21
+  error: {
22
+    type: String,
23
+    default: null,
24
+  },
25
+  modelValue: null,
26
+})
27
+
28
+defineEmits(['update:modelValue'])
29
+
30
+const isError = computed(() => (props.error ? true : false))
31
+
32
+const forLabel = computed(() => props.label.toLowerCase().replace(/\s+/g, '-'))
33
+
34
+const ariaDescribedbyLabel = computed(() => props.label.toLowerCase().replace(/\s+/g, '-') + '-help')
35
+</script>
36
+
37
+<template>
38
+  <div class="field">
39
+    <label :for="forLabel">{{ label }}</label>
40
+
41
+    <InputText
42
+      class="w-full"
43
+      :class="{ 'p-invalid': isError }"
44
+      :id="forLabel"
45
+      :aria-describedby="ariaDescribedbyLabel"
46
+      :model-value="modelValue"
47
+      :type="type"
48
+      :placeholder="placeholder"
49
+      :value="modelValue"
50
+      :disabled="disabled"
51
+      @input="$emit('update:modelValue', $event.target.value)"
52
+    />
53
+
54
+    <small v-if="error" :id="ariaDescribedbyLabel" :class="{ 'p-error': isError }">
55
+      {{ error }}
56
+    </small>
57
+  </div>
58
+</template>

+ 275
- 0
resources/js/components/AppMenu.vue 查看文件

@@ -0,0 +1,275 @@
1
+<script>
2
+import { ConnectedOverlayScrollHandler, DomHandler, ZIndexUtils } from 'primevue/utils'
3
+import OverlayEventBus from 'primevue/overlayeventbus'
4
+import Menuitem from './AppMenuItem.vue'
5
+export default {
6
+  name: 'Menu',
7
+  emits: ['show', 'hide'],
8
+  inheritAttrs: false,
9
+  props: {
10
+    popup: {
11
+      type: Boolean,
12
+      default: false,
13
+    },
14
+    model: {
15
+      type: Array,
16
+      default: null,
17
+    },
18
+    appendTo: {
19
+      type: String,
20
+      default: 'body',
21
+    },
22
+    autoZIndex: {
23
+      type: Boolean,
24
+      default: true,
25
+    },
26
+    baseZIndex: {
27
+      type: Number,
28
+      default: 0,
29
+    },
30
+    exact: {
31
+      type: Boolean,
32
+      default: true,
33
+    },
34
+  },
35
+  data() {
36
+    return {
37
+      overlayVisible: false,
38
+    }
39
+  },
40
+  target: null,
41
+  outsideClickListener: null,
42
+  scrollHandler: null,
43
+  resizeListener: null,
44
+  container: null,
45
+  beforeUnmount() {
46
+    this.unbindResizeListener()
47
+    this.unbindOutsideClickListener()
48
+    if (this.scrollHandler) {
49
+      this.scrollHandler.destroy()
50
+      this.scrollHandler = null
51
+    }
52
+    this.target = null
53
+    if (this.container && this.autoZIndex) {
54
+      ZIndexUtils.clear(this.container)
55
+    }
56
+    this.container = null
57
+  },
58
+  methods: {
59
+    itemClick(event) {
60
+      const item = event.item
61
+      if (item.disabled) {
62
+        return
63
+      }
64
+      if (item.command) {
65
+        item.command(event)
66
+      }
67
+      if (item.to && event.navigate) {
68
+        event.navigate(event.originalEvent)
69
+      }
70
+      this.hide()
71
+    },
72
+    toggle(event) {
73
+      if (this.overlayVisible) this.hide()
74
+      else this.show(event)
75
+    },
76
+    show(event) {
77
+      this.overlayVisible = true
78
+      this.target = event.currentTarget
79
+    },
80
+    hide() {
81
+      this.overlayVisible = false
82
+      this.target = null
83
+    },
84
+    onEnter(el) {
85
+      this.alignOverlay()
86
+      this.bindOutsideClickListener()
87
+      this.bindResizeListener()
88
+      this.bindScrollListener()
89
+      if (this.autoZIndex) {
90
+        ZIndexUtils.set('menu', el, this.baseZIndex + this.$primevue.config.zIndex.menu)
91
+      }
92
+      this.$emit('show')
93
+    },
94
+    onLeave() {
95
+      this.unbindOutsideClickListener()
96
+      this.unbindResizeListener()
97
+      this.unbindScrollListener()
98
+      this.$emit('hide')
99
+    },
100
+    onAfterLeave(el) {
101
+      if (this.autoZIndex) {
102
+        ZIndexUtils.clear(el)
103
+      }
104
+    },
105
+    alignOverlay() {
106
+      DomHandler.absolutePosition(this.container, this.target)
107
+      this.container.style.minWidth = DomHandler.getOuterWidth(this.target) + 'px'
108
+    },
109
+    bindOutsideClickListener() {
110
+      if (!this.outsideClickListener) {
111
+        this.outsideClickListener = (event) => {
112
+          if (
113
+            this.overlayVisible &&
114
+            this.container &&
115
+            !this.container.contains(event.target) &&
116
+            !this.isTargetClicked(event)
117
+          ) {
118
+            this.hide()
119
+          }
120
+        }
121
+        document.addEventListener('click', this.outsideClickListener)
122
+      }
123
+    },
124
+    unbindOutsideClickListener() {
125
+      if (this.outsideClickListener) {
126
+        document.removeEventListener('click', this.outsideClickListener)
127
+        this.outsideClickListener = null
128
+      }
129
+    },
130
+    bindScrollListener() {
131
+      if (!this.scrollHandler) {
132
+        this.scrollHandler = new ConnectedOverlayScrollHandler(this.target, () => {
133
+          if (this.overlayVisible) {
134
+            this.hide()
135
+          }
136
+        })
137
+      }
138
+      this.scrollHandler.bindScrollListener()
139
+    },
140
+    unbindScrollListener() {
141
+      if (this.scrollHandler) {
142
+        this.scrollHandler.unbindScrollListener()
143
+      }
144
+    },
145
+    bindResizeListener() {
146
+      if (!this.resizeListener) {
147
+        this.resizeListener = () => {
148
+          if (this.overlayVisible) {
149
+            this.hide()
150
+          }
151
+        }
152
+        window.addEventListener('resize', this.resizeListener)
153
+      }
154
+    },
155
+    unbindResizeListener() {
156
+      if (this.resizeListener) {
157
+        window.removeEventListener('resize', this.resizeListener)
158
+        this.resizeListener = null
159
+      }
160
+    },
161
+    isTargetClicked(event) {
162
+      return this.target && (this.target === event.target || this.target.contains(event.target))
163
+    },
164
+    visible(item) {
165
+      return typeof item.visible === 'function' ? item.visible() : item.visible !== false
166
+    },
167
+    label(item) {
168
+      return typeof item.label === 'function' ? item.label() : item.label
169
+    },
170
+    containerRef(el) {
171
+      this.container = el
172
+    },
173
+    onOverlayClick(event) {
174
+      OverlayEventBus.emit('overlay-click', {
175
+        originalEvent: event,
176
+        target: this.target,
177
+      })
178
+    },
179
+  },
180
+  computed: {
181
+    containerClass() {
182
+      return [
183
+        'p-menu p-component',
184
+        {
185
+          'p-menu-overlay': this.popup,
186
+          'p-input-filled': this.$primevue.config.inputStyle === 'filled',
187
+          'p-ripple-disabled': this.$primevue.config.ripple === false,
188
+        },
189
+      ]
190
+    },
191
+  },
192
+  components: {
193
+    Menuitem: Menuitem,
194
+  },
195
+}
196
+</script>
197
+
198
+<template>
199
+  <Teleport :to="appendTo" :disabled="!popup">
200
+    <transition name="p-connected-overlay" @enter="onEnter" @leave="onLeave" @after-leave="onAfterLeave">
201
+      <div
202
+        :ref="containerRef"
203
+        :class="containerClass"
204
+        v-if="popup ? overlayVisible : true"
205
+        v-bind="$attrs"
206
+        @click="onOverlayClick"
207
+      >
208
+        <ul class="p-menu-list p-reset" role="menu">
209
+          <template v-for="(item, i) of model" :key="label(item) + i.toString()">
210
+            <template v-if="item.items && visible(item) && !item.separator">
211
+              <li class="p-submenu-header" v-if="item.items">
212
+                <slot name="item" :item="item">{{ label(item) }}</slot>
213
+              </li>
214
+              <template v-for="(child, j) of item.items" :key="child.label + i + j">
215
+                <Menuitem
216
+                  v-if="visible(child) && !child.separator"
217
+                  :item="child"
218
+                  @click="itemClick"
219
+                  :template="$slots.item"
220
+                  :exact="exact"
221
+                />
222
+                <li
223
+                  v-else-if="visible(child) && child.separator"
224
+                  :class="['p-menu-separator', child.class]"
225
+                  :style="child.style"
226
+                  :key="'separator' + i + j"
227
+                  role="separator"
228
+                ></li>
229
+              </template>
230
+            </template>
231
+            <li
232
+              v-else-if="visible(item) && item.separator"
233
+              :class="['p-menu-separator', item.class]"
234
+              :style="item.style"
235
+              :key="'separator' + i.toString()"
236
+              role="separator"
237
+            ></li>
238
+            <Menuitem
239
+              v-else
240
+              :key="label(item) + i.toString()"
241
+              :item="item"
242
+              @click="itemClick"
243
+              :template="$slots.item"
244
+              :exact="exact"
245
+            />
246
+          </template>
247
+        </ul>
248
+      </div>
249
+    </transition>
250
+  </Teleport>
251
+</template>
252
+
253
+<style>
254
+.p-menu-overlay {
255
+  position: absolute;
256
+  top: 0;
257
+  left: 0;
258
+}
259
+.p-menu ul {
260
+  margin: 0;
261
+  padding: 0;
262
+  list-style: none;
263
+}
264
+.p-menu .p-menuitem-link {
265
+  cursor: pointer;
266
+  display: flex;
267
+  align-items: center;
268
+  text-decoration: none;
269
+  overflow: hidden;
270
+  position: relative;
271
+}
272
+.p-menu .p-menuitem-text {
273
+  line-height: 1;
274
+}
275
+</style>

+ 83
- 0
resources/js/components/AppMenuItem.vue 查看文件

@@ -0,0 +1,83 @@
1
+<script>
2
+import Ripple from 'primevue/ripple'
3
+import { Link } from '@inertiajs/inertia-vue3'
4
+export default {
5
+  name: 'Menuitem',
6
+  inheritAttrs: false,
7
+  components: {
8
+    Link,
9
+  },
10
+  emits: ['click'],
11
+  props: {
12
+    item: null,
13
+    template: null,
14
+    exact: null,
15
+  },
16
+  methods: {
17
+    onClick(event, navigate) {
18
+      this.$emit('click', {
19
+        originalEvent: event,
20
+        item: this.item,
21
+        navigate: navigate,
22
+      })
23
+    },
24
+    linkClass(item) {
25
+      return [
26
+        'p-menuitem-link',
27
+        {
28
+          'p-disabled': this.disabled(item),
29
+        },
30
+      ]
31
+    },
32
+    visible() {
33
+      return typeof this.item.visible === 'function' ? this.item.visible() : this.item.visible !== false
34
+    },
35
+    disabled(item) {
36
+      return typeof item.disabled === 'function' ? item.disabled() : item.disabled
37
+    },
38
+    label() {
39
+      return typeof this.item.label === 'function' ? this.item.label() : this.item.label
40
+    },
41
+  },
42
+  computed: {
43
+    containerClass() {
44
+      return ['p-menuitem', this.item.class]
45
+    },
46
+  },
47
+  directives: {
48
+    ripple: Ripple,
49
+  },
50
+}
51
+</script>
52
+
53
+<template>
54
+  <li :class="containerClass" role="none" :style="item.style" v-if="visible()">
55
+    <template v-if="!template">
56
+      <Link
57
+        v-if="item.to && !disabled(item)"
58
+        v-ripple
59
+        role="menuitem"
60
+        :href="item.to"
61
+        :class="linkClass(item)"
62
+        @click="onClick($event, navigate)"
63
+      >
64
+        <span :class="['p-menuitem-icon', item.icon]"></span>
65
+        <span class="p-menuitem-text">{{ label() }}</span>
66
+      </Link>
67
+      <a
68
+        v-else
69
+        :href="item.url"
70
+        :class="linkClass(item)"
71
+        @click="onClick"
72
+        :target="item.target"
73
+        role="menuitem"
74
+        :tabindex="disabled(item) ? null : '0'"
75
+        v-ripple
76
+      >
77
+        <span :class="['p-menuitem-icon', item.icon]"></span>
78
+        <span class="p-menuitem-text">{{ label() }}</span>
79
+      </a>
80
+    </template>
81
+    <component v-else :is="template" :item="item"></component>
82
+  </li>
83
+</template>

+ 32
- 0
resources/js/components/AppPagination.vue 查看文件

@@ -0,0 +1,32 @@
1
+<script setup>
2
+import { Link } from '@inertiajs/inertia-vue3'
3
+
4
+defineProps({
5
+  links: Array,
6
+})
7
+</script>
8
+
9
+<template>
10
+  <nav v-if="links.length > 3" class="p-paginator p-component flex justify-content-start">
11
+    <div class="p-paginator-pages">
12
+      <template v-for="(link, key) in links">
13
+        <div
14
+          v-if="link.url === null"
15
+          :key="`link-${key}`"
16
+          class="p-paginator-page p-paginator-element p-link"
17
+          :class="{ 'p-disabled': link }"
18
+          v-html="link.label"
19
+        />
20
+
21
+        <Link
22
+          v-if="link.url !== null"
23
+          :key="`link-${key}`"
24
+          :href="link.url"
25
+          :class="{ 'p-highlight': link.active }"
26
+          class="p-paginator-page p-paginator-element p-link"
27
+          v-html="link.label"
28
+        />
29
+      </template>
30
+    </div>
31
+  </nav>
32
+</template>

+ 49
- 0
resources/js/components/AppPassword.vue 查看文件

@@ -0,0 +1,49 @@
1
+<script setup>
2
+import { computed } from 'vue'
3
+
4
+const props = defineProps({
5
+  label: {
6
+    type: String,
7
+    required: true,
8
+  },
9
+  placeholder: {
10
+    type: String,
11
+    required: true,
12
+  },
13
+  error: {
14
+    type: String,
15
+    default: null,
16
+  },
17
+  modelValue: null,
18
+})
19
+
20
+defineEmits(['update:modelValue'])
21
+
22
+const isError = computed(() => (props.error ? true : false))
23
+
24
+const forLabel = computed(() => props.label.toLowerCase().replace(/\s+/g, '-'))
25
+
26
+const ariaDescribedbyLabel = computed(() => props.label.toLowerCase().replace(/\s+/g, '-') + '-help')
27
+</script>
28
+
29
+<template>
30
+  <div class="field">
31
+    <label :for="forLabel">{{ label }}</label>
32
+
33
+    <Password
34
+      class="w-full"
35
+      input-class="w-full"
36
+      :id="forLabel"
37
+      :placeholder="placeholder"
38
+      :aria-describedby="ariaDescribedbyLabel"
39
+      :toggleMask="true"
40
+      :value="modelValue"
41
+      :model-value="modelValue"
42
+      @input="$emit('update:modelValue', $event.target.value)"
43
+    />
44
+
45
+    <small v-if="error" :id="ariaDescribedbyLabel" :class="{ 'p-error': isError }">
46
+      {{ error }}
47
+    </small>
48
+  </div>
49
+</template>

+ 1
- 1
resources/js/components/AppTopBar.vue 查看文件

@@ -32,7 +32,7 @@ defineEmits(['menu-toggle'])
32 32
         <span class="hidden lg:inline">{{ $page.props.auth.user.name }}</span>
33 33
       </li>
34 34
       <li>
35
-        <Link class="p-link layout-topbar-button">
35
+        <Link :href="route('users.show', $page.props.auth.user.id)" class="p-link layout-topbar-button">
36 36
           <i class="pi pi-user"></i>
37 37
           <span>Profil Saya</span>
38 38
         </Link>

+ 78
- 0
resources/js/pages/user/Create.vue 查看文件

@@ -0,0 +1,78 @@
1
+<script setup>
2
+import { computed, watch } from 'vue'
3
+import { useForm, Head, usePage } from '@inertiajs/inertia-vue3'
4
+import AppInputText from '@/components/AppInputText.vue'
5
+import AppDropdown from '@/components/AppDropdown.vue'
6
+import AppLayout from '@/layouts/AppLayout.vue'
7
+
8
+defineProps({
9
+  roles: Array,
10
+})
11
+
12
+const errors = computed(() => usePage().props.value.errors)
13
+
14
+watch(errors, () => {
15
+  form.clearErrors()
16
+})
17
+
18
+const form = useForm({
19
+  name: '',
20
+  phone: '',
21
+  email: '',
22
+  role_id: '',
23
+})
24
+
25
+const submit = () => {
26
+  form.post(route('users.store'), { onSuccess: () => form.reset() })
27
+}
28
+</script>
29
+
30
+<template>
31
+  <Head title="Tambah User" />
32
+
33
+  <AppLayout>
34
+    <div class="grid">
35
+      <div class="col-12 lg:col-8">
36
+        <Card>
37
+          <template #content>
38
+            <div class="grid">
39
+              <div class="col-12 md:col-6">
40
+                <AppInputText label="Nama" placeholder="nama" :error="form.errors.name" v-model="form.name" />
41
+              </div>
42
+
43
+              <div class="col-12 md:col-6">
44
+                <AppInputText label="Nomor HP" placeholder="nomor hp" :error="form.errors.phone" v-model="form.phone" />
45
+              </div>
46
+
47
+              <div class="col-12 md:col-6">
48
+                <AppInputText label="Email" placeholder="email" :error="form.errors.email" v-model="form.email" />
49
+              </div>
50
+
51
+              <div class="col-12 md:col-6">
52
+                <AppDropdown
53
+                  label="Hak Akses"
54
+                  placeholder="pilih satu"
55
+                  v-model="form.role_id"
56
+                  :options="roles"
57
+                  :error="form.errors.role_id"
58
+                />
59
+              </div>
60
+            </div>
61
+          </template>
62
+
63
+          <template #footer>
64
+            <div class="flex flex-column md:flex-row justify-content-end">
65
+              <Button
66
+                label="Simpan"
67
+                icon="pi pi-check"
68
+                class="p-button-outlined"
69
+                :disabled="form.processing"
70
+                @click="submit"
71
+              />
72
+            </div>
73
+          </template>
74
+        </Card>
75
+      </div>
76
+    </div>
77
+  </AppLayout>
78
+</template>

+ 137
- 0
resources/js/pages/user/Edit.vue 查看文件

@@ -0,0 +1,137 @@
1
+<script setup>
2
+import { ref, watch, computed } from 'vue'
3
+import { Inertia } from '@inertiajs/inertia'
4
+import { useForm, Head, usePage } from '@inertiajs/inertia-vue3'
5
+import AppInputText from '@/components/AppInputText.vue'
6
+import AppDropdown from '@/components/AppDropdown.vue'
7
+import AppButton from '@/components/AppButton.vue'
8
+import AppDialog from '@/components/AppDialog.vue'
9
+import AppLayout from '@/layouts/AppLayout.vue'
10
+
11
+const props = defineProps({
12
+  user: Object,
13
+  roles: Array,
14
+})
15
+
16
+const form = useForm({
17
+  name: props.user.name,
18
+  phone: props.user.phone,
19
+  email: props.user.email,
20
+  role_id: props.user.role_id,
21
+})
22
+
23
+const submit = () => {
24
+  form.put(route('users.update', props.user.id))
25
+}
26
+
27
+const visibleDialog = ref(false)
28
+
29
+const confirmDialog = () => {
30
+  visibleDialog.value = true
31
+}
32
+
33
+const onAgree = (id) => Inertia.delete(route('users.destroy', id))
34
+
35
+const onCancel = () => (visibleDialog.value = false)
36
+
37
+const errors = computed(() => usePage().props.value.errors)
38
+
39
+watch(errors, () => {
40
+  form.clearErrors()
41
+})
42
+</script>
43
+
44
+<template>
45
+  <Head title="Ubah User" />
46
+
47
+  <AppLayout>
48
+    <div class="grid">
49
+      <div class="col-12 lg:col-8">
50
+        <Card>
51
+          <template #content>
52
+            <div class="grid">
53
+              <div class="col-12 md:col-6">
54
+                <AppInputText
55
+                  label="Nama"
56
+                  placeholder="nama"
57
+                  :disabled="user.role_id !== 1"
58
+                  :error="form.errors.name"
59
+                  v-model="form.name"
60
+                />
61
+              </div>
62
+
63
+              <div class="col-12 md:col-6">
64
+                <AppInputText
65
+                  label="Nomor HP"
66
+                  placeholder="nomor hp"
67
+                  :disabled="user.role_id !== 1"
68
+                  :error="form.errors.phone"
69
+                  v-model="form.phone"
70
+                />
71
+              </div>
72
+
73
+              <div class="col-12 md:col-6">
74
+                <AppInputText
75
+                  label="Email"
76
+                  placeholder="email"
77
+                  :disabled="user.role_id !== 1"
78
+                  :error="form.errors.email"
79
+                  v-model="form.email"
80
+                />
81
+              </div>
82
+
83
+              <div v-if="user.role_id !== 1" class="col-12 md:col-6">
84
+                <AppDropdown
85
+                  label="Hak Akses"
86
+                  placeholder="Pilih satu"
87
+                  v-model="form.role_id"
88
+                  :options="roles"
89
+                  :error="form.errors.role_id"
90
+                />
91
+              </div>
92
+            </div>
93
+          </template>
94
+
95
+          <template #footer>
96
+            <div class="grid">
97
+              <div class="col-12 md:col-6 flex flex-column md:flex-row justify-content-center md:justify-content-start">
98
+                <AppDialog
99
+                  message="Yakin akan menghapus data ini?"
100
+                  v-model:visible="visibleDialog"
101
+                  @agree="onAgree(user.id)"
102
+                  @cancel="onCancel"
103
+                />
104
+
105
+                <Button
106
+                  v-if="$page.props.auth.user.role_id !== user.role_id"
107
+                  label="Hapus"
108
+                  icon="pi pi-trash"
109
+                  class="p-button-outlined p-button-danger"
110
+                  @click="confirmDialog"
111
+                />
112
+              </div>
113
+
114
+              <div class="col-12 md:col-6 flex flex-column md:flex-row justify-content-center md:justify-content-end">
115
+                <AppButton
116
+                  label="Blokir"
117
+                  icon="pi pi-ban"
118
+                  method="delete"
119
+                  class="p-button-outlined p-button-danger md:mr-3 mb-3 md:mb-0"
120
+                  :href="route('users.block', user.id)"
121
+                />
122
+
123
+                <Button
124
+                  label="Simpan"
125
+                  class="p-button-outlined"
126
+                  icon="pi pi-check"
127
+                  :disabled="form.processing"
128
+                  @click="submit"
129
+                />
130
+              </div>
131
+            </div>
132
+          </template>
133
+        </Card>
134
+      </div>
135
+    </div>
136
+  </AppLayout>
137
+</template>

+ 82
- 0
resources/js/pages/user/Index.vue 查看文件

@@ -0,0 +1,82 @@
1
+<script setup>
2
+import { watch } from 'vue'
3
+import { Inertia } from '@inertiajs/inertia'
4
+import { Head, useForm } from '@inertiajs/inertia-vue3'
5
+import throttle from 'lodash/throttle'
6
+import pickBy from 'lodash/pickBy'
7
+import AppLayout from '@/layouts/AppLayout.vue'
8
+import AppButton from '@/components/AppButton.vue'
9
+import AppPagination from '@/components/AppPagination.vue'
10
+
11
+import TableHeader from './TableHeader'
12
+
13
+const props = defineProps({
14
+  users: Object,
15
+  filters: Object,
16
+})
17
+
18
+const filterForm = useForm({
19
+  search: props.filters.search,
20
+})
21
+
22
+watch(
23
+  filterForm,
24
+  throttle(() => {
25
+    Inertia.get('/users', pickBy({ search: filterForm.search }), { preserveState: true })
26
+  }, 300)
27
+)
28
+</script>
29
+
30
+<template>
31
+  <Head title="Daftar User" />
32
+
33
+  <AppLayout>
34
+    <DataTable
35
+      responsiveLayout="scroll"
36
+      columnResizeMode="expand"
37
+      :value="users.data"
38
+      :rowHover="true"
39
+      :stripedRows="true"
40
+    >
41
+      <template #header>
42
+        <h1>User</h1>
43
+
44
+        <div class="grid">
45
+          <div class="col-12 md:col-8">
46
+            <div class="flex align-items-center">
47
+              <InputText class="w-full md:w-27rem" placeholder="cari..." v-model="filterForm.search" />
48
+            </div>
49
+          </div>
50
+
51
+          <div class="col-12 md:col-4 flex flex-column md:flex-row justify-content-end">
52
+            <AppButton
53
+              label="Tambah User"
54
+              icon="pi pi-pencil"
55
+              class="p-button-outlined"
56
+              :href="route('users.create')"
57
+            />
58
+          </div>
59
+        </div>
60
+      </template>
61
+
62
+      <Column
63
+        v-for="tableHeader in TableHeader"
64
+        :field="tableHeader.field"
65
+        :header="tableHeader.header"
66
+        :key="tableHeader.field"
67
+      />
68
+
69
+      <Column>
70
+        <template #body="{ data }">
71
+          <AppButton
72
+            icon="pi pi-angle-double-right"
73
+            class="p-button-icon-only p-button-rounded p-button-text"
74
+            :href="route('users.edit', data.id)"
75
+          />
76
+        </template>
77
+      </Column>
78
+    </DataTable>
79
+
80
+    <AppPagination :links="users.links" />
81
+  </AppLayout>
82
+</template>

+ 130
- 0
resources/js/pages/user/Show.vue 查看文件

@@ -0,0 +1,130 @@
1
+<script setup>
2
+import { computed, watch } from 'vue'
3
+import { Head, useForm, usePage } from '@inertiajs/inertia-vue3'
4
+import AppLayout from '@/layouts/AppLayout.vue'
5
+import AppInputText from '@/components/AppInputText.vue'
6
+import AppPassword from '@/components/AppPassword.vue'
7
+
8
+const props = defineProps({
9
+  user: Object,
10
+  roles: Array,
11
+})
12
+
13
+const errors = computed(() => usePage().props.value.errors)
14
+
15
+watch(errors, () => {
16
+  form.clearErrors()
17
+  formChangePassword.clearErrors()
18
+})
19
+
20
+const form = useForm({
21
+  name: props.user.name,
22
+  phone: props.user.phone,
23
+  email: props.user.email,
24
+  role_id: props.user.role_id,
25
+})
26
+
27
+const submit = () => {
28
+  form.put(route('users.update', props.user.id))
29
+}
30
+
31
+const formChangePassword = useForm({
32
+  old_password: '',
33
+  new_password: '',
34
+  new_password_confirmation: '',
35
+})
36
+
37
+const changePassword = () => {
38
+  formChangePassword.post(route('users.change-password', props.user.id), {
39
+    onSuccess: () => formChangePassword.reset(),
40
+  })
41
+}
42
+</script>
43
+
44
+<template>
45
+  <Head title="Profil Saya" />
46
+
47
+  <AppLayout>
48
+    <div class="grid">
49
+      <div class="col-12 md:col-8">
50
+        <Card>
51
+          <template #title>Profil Saya</template>
52
+
53
+          <template #content>
54
+            <TabView lazy>
55
+              <TabPanel header="Ubah Profil">
56
+                <div class="grid">
57
+                  <div class="col-12 md:col-6">
58
+                    <AppInputText label="Nama" placeholder="nama" :error="form.errors.name" v-model="form.name" />
59
+                  </div>
60
+
61
+                  <div class="col-12 md:col-6">
62
+                    <AppInputText
63
+                      label="Nomor HP"
64
+                      placeholder="nomor hp"
65
+                      :error="form.errors.phone"
66
+                      v-model="form.phone"
67
+                    />
68
+                  </div>
69
+
70
+                  <div class="col-12 md:col-6">
71
+                    <AppInputText label="Email" placeholder="email" :error="form.errors.email" v-model="form.email" />
72
+                  </div>
73
+
74
+                  <div class="col-12 flex justify-content-end">
75
+                    <Button
76
+                      label="Simpan"
77
+                      icon="pi pi-check"
78
+                      class="p-button-outlined"
79
+                      :disabled="form.processing"
80
+                      @click="submit"
81
+                    />
82
+                  </div>
83
+                </div>
84
+              </TabPanel>
85
+              <TabPanel header="Ubah Password">
86
+                <div class="grid">
87
+                  <div class="col-12">
88
+                    <AppPassword
89
+                      label="Password Lama"
90
+                      placeholder="password lama"
91
+                      :error="formChangePassword.errors.old_password"
92
+                      v-model="formChangePassword.old_password"
93
+                    />
94
+                  </div>
95
+
96
+                  <div class="col-12">
97
+                    <AppPassword
98
+                      label="Password Baru"
99
+                      placeholder="password baru"
100
+                      :error="formChangePassword.errors.new_password"
101
+                      v-model="formChangePassword.new_password"
102
+                    />
103
+                  </div>
104
+
105
+                  <div class="col-12">
106
+                    <AppPassword
107
+                      label="Konfirmasi Password"
108
+                      placeholder="konfirmasi password"
109
+                      v-model="formChangePassword.new_password_confirmation"
110
+                    />
111
+                  </div>
112
+
113
+                  <div class="col-12 flex justify-content-end">
114
+                    <Button
115
+                      label="Simpan"
116
+                      icon="pi pi-check"
117
+                      class="p-button-outlined"
118
+                      :disabled="formChangePassword.processing"
119
+                      @click="changePassword"
120
+                    />
121
+                  </div>
122
+                </div>
123
+              </TabPanel>
124
+            </TabView>
125
+          </template>
126
+        </Card>
127
+      </div>
128
+    </div>
129
+  </AppLayout>
130
+</template>

+ 7
- 0
resources/js/pages/user/TableHeader.js 查看文件

@@ -0,0 +1,7 @@
1
+export default [
2
+  { field: 'name', header: 'Nama' },
3
+  { field: 'phone', header: 'No HP' },
4
+  { field: 'email', header: 'Email' },
5
+  { field: 'role', header: 'Hak Akses' },
6
+  { field: 'status', header: 'Status' },
7
+]

+ 2
- 8
resources/js/utils/menu.js 查看文件

@@ -6,14 +6,8 @@ export default {
6 6
       items: [{ label: 'Dashboard', icon: 'pi pi-home', to: '/dashboards', component: 'home/Index' }],
7 7
     },
8 8
     {
9
-      label: 'Menu',
10
-      items: [
11
-        {
12
-          label: 'Submenu 1',
13
-          icon: 'pi pi-bookmark',
14
-          items: [{ label: 'Submenu 1.1', icon: 'pi pi-bookmark' }],
15
-        },
16
-      ],
9
+      label: 'Master',
10
+      items: [{ label: 'User', icon: 'pi pi-user', to: '/users', component: 'user/Index' }],
17 11
     },
18 12
   ],
19 13
 

+ 5
- 0
routes/web.php 查看文件

@@ -1,6 +1,7 @@
1 1
 <?php
2 2
 
3 3
 use App\Http\Controllers\DashboardController;
4
+use App\Http\Controllers\UserController;
4 5
 use Illuminate\Support\Facades\Route;
5 6
 
6 7
 /*
@@ -18,6 +19,10 @@ Route::middleware(['auth', 'verified'])->group(function () {
18 19
     Route::get('/', DashboardController::class);
19 20
 
20 21
     Route::get('/dashboards', DashboardController::class);
22
+
23
+    Route::delete('/users/block/{user}', [UserController::class, 'block'])->name('users.block');
24
+    Route::post('/users/change-password/{user}', [UserController::class, 'changePassword'])->name('users.change-password');
25
+    Route::resource('/users', UserController::class);
21 26
 });
22 27
 
23 28
 require __DIR__ . '/auth.php';