Added: User Management, Roles & Permissions

This commit is contained in:
Fahim Anzam Dip 2021-07-19 19:03:19 +06:00
parent 0d7d2c747e
commit 1cec992366
48 changed files with 1035 additions and 173 deletions

View File

@ -16,7 +16,6 @@ class CreateProductsTable extends Migration
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('category_id');
$table->string('product_image')->nullable();
$table->string('product_name');
$table->string('product_code')->unique()->nullable();
$table->string('product_barcode_symbology')->nullable();

View File

@ -9,6 +9,7 @@ use Spatie\MediaLibrary\InteractsWithMedia;
class Product extends Model implements HasMedia
{
use HasFactory, InteractsWithMedia;
protected $guarded = [];
@ -18,4 +19,9 @@ class Product extends Model implements HasMedia
public function category() {
return $this->belongsTo(Category::class, 'category_id', 'id');
}
public function registerMediaCollections(): void {
$this->addMediaCollection('default')
->useFallbackUrl('/images/fallback_product_image.png');
}
}

View File

@ -5,26 +5,23 @@ namespace Modules\Product\Http\Controllers;
use Illuminate\Contracts\Support\Renderable;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Gate;
use Modules\Product\Entities\Category;
use App\DataTables\ProductCategoriesDataTable;
class CategoriesController extends Controller
{
/**
* Display a listing of the resource.
* @return Renderable
*/
public function index(ProductCategoriesDataTable $dataTable) {
abort_if(Gate::denies('access_product_categories'), 403);
return $dataTable->render('product::categories.index');
}
/**
* Store a newly created resource in storage.
* @param Request $request
* @return Renderable
*/
public function store(Request $request) {
abort_if(Gate::denies('access_product_categories'), 403);
$request->validate([
'category_code' => 'required|unique:categories,category_code',
'category_name' => 'required'
@ -40,24 +37,19 @@ class CategoriesController extends Controller
return redirect()->back();
}
/**
* Show the form for editing the specified resource.
* @param int $id
* @return Renderable
*/
public function edit($id) {
abort_if(Gate::denies('access_product_categories'), 403);
$category = Category::findOrFail($id);
return view('product::categories.edit', compact('category'));
}
/**
* Update the specified resource in storage.
* @param Request $request
* @param int $id
* @return Renderable
*/
public function update(Request $request, $id) {
abort_if(Gate::denies('access_product_categories'), 403);
$request->validate([
'category_code' => 'required|unique:categories,category_code,' . $id,
'category_name' => 'required'
@ -73,12 +65,10 @@ class CategoriesController extends Controller
return redirect()->route('product-categories.index');
}
/**
* Remove the specified resource from storage.
* @param int $id
* @return Renderable
*/
public function destroy($id) {
abort_if(Gate::denies('access_product_categories'), 403);
$category = Category::findOrFail($id);
if ($category->products->isNotEmpty()) {

View File

@ -6,6 +6,7 @@ use App\DataTables\ProductDataTable;
use Illuminate\Contracts\Support\Renderable;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Modules\Product\Entities\Product;
use Modules\Product\Http\Requests\ProductCreateRequest;
@ -15,27 +16,20 @@ use Modules\Upload\Entities\Upload;
class ProductController extends Controller
{
/**
* Display a listing of the resource.
* @return Renderable
*/
public function index(ProductDataTable $dataTable) {
abort_if(Gate::denies('access_products'), 403);
return $dataTable->render('product::products.index');
}
/**
* Show the form for creating a new resource.
* @return Renderable
*/
public function create() {
abort_if(Gate::denies('create_products'), 403);
return view('product::products.create');
}
/**
* Store a newly created resource in storage.
* @param Request $request
* @return Renderable
*/
public function store(ProductCreateRequest $request) {
$product = Product::create($request->except('image'));
@ -55,38 +49,30 @@ class ProductController extends Controller
return redirect()->route('products.index');
}
/**
* Show the details for the specified resource.
* @param int $id
* @return Renderable
*/
public function show(Product $product) {
abort_if(Gate::denies('show_products'), 403);
return view('product::products.show', compact('product'));
}
/**
* Show the form for editing the specified resource.
* @param int $id
* @return Renderable
*/
public function edit(Product $product) {
abort_if(Gate::denies('edit_products'), 403);
return view('product::products.edit', compact('product'));
}
/**
* Update the specified resource in storage.
* @param Request $request
* @param int $id
* @return Renderable
*/
public function update(ProductUpdateRequest $request, Product $product) {
$product->update($request->except('image'));
if ($request->has('image')) {
$tempFile = Upload::where('folder', $request->image)->first();
$media = $product->getMedia();
$media[0]->delete();
if ($product->getFirstMedia()) {
$product->getFirstMedia()->delete();
}
if ($tempFile) {
$product->addMedia(Storage::path('public/temp/' . $request->image . '/' . $tempFile->filename))->toMediaCollection();
@ -101,12 +87,10 @@ class ProductController extends Controller
return redirect()->route('products.index');
}
/**
* Remove the specified resource from storage.
* @param int $id
* @return Renderable
*/
public function destroy(Product $product) {
abort_if(Gate::denies('delete_products'), 403);
$product->delete();
toast('Product Deleted!', 'warning');

View File

@ -3,6 +3,7 @@
namespace Modules\Product\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
class ProductCreateRequest extends FormRequest
{
@ -35,6 +36,6 @@ class ProductCreateRequest extends FormRequest
*/
public function authorize()
{
return true;
return Gate::allows('create_products');
}
}

View File

@ -3,6 +3,7 @@
namespace Modules\Product\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\Rule;
class ProductUpdateRequest extends FormRequest
@ -36,6 +37,6 @@ class ProductUpdateRequest extends FormRequest
*/
public function authorize()
{
return true;
return Gate::allows('edit_products');
}
}

View File

@ -15,6 +15,7 @@
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-md-7">
@include('utils.alerts')
<div class="card">
<div class="card-body">
<form action="{{ route('product-categories.update', $category->id) }}" method="POST">

View File

@ -18,6 +18,7 @@
<div class="container-fluid">
<div class="row">
<div class="col-12">
@include('utils.alerts')
<div class="card">
<div class="card-body">
<!-- Button trigger modal -->

View File

@ -1,9 +1,14 @@
@can('edit_products')
<a href="{{ route('products.edit', $data->id) }}" class="btn btn-info btn-sm">
<i class="bi bi-pencil"></i>
</a>
@endcan
@can('show_products')
<a href="{{ route('products.show', $data->id) }}" class="btn btn-primary btn-sm">
<i class="bi bi-eye"></i>
</a>
@endcan
@can('delete_products')
<button id="delete" class="btn btn-danger btn-sm" onclick="
event.preventDefault();
if (confirm('Are you sure? It will delete the data permanently!')) {
@ -16,3 +21,4 @@
@method('delete')
</form>
</button>
@endcan

View File

@ -13,6 +13,10 @@ class UploadController extends Controller
{
public function filepondUpload(Request $request) {
if (!$request->ajax()) {
return back();
}
$request->validate([
'image' => 'required|image|mimes:png,jpeg,jpg'
]);
@ -36,4 +40,17 @@ class UploadController extends Controller
return false;
}
public function filepondDelete(Request $request) {
if (!$request->ajax()) {
return back();
}
$upload = Upload::where('folder', $request->getContent())->first();
Storage::deleteDirectory('public/temp/' . $upload->folder);
$upload->delete();
return response(null);
}
}

View File

@ -12,7 +12,10 @@
*/
Route::group(['middleware' => 'auth'], function () {
Route::post('/filepond/upload', 'UploadController@filepondUpload')->name('filepond.upload');
Route::delete('/filepond/delete', 'UploadController@filepondDelete')->name('filepond.delete');
});

View File

@ -27,6 +27,7 @@ class PermissionsTableSeeder extends Seeder
'edit_products',
'delete_products',
'access_product_categories',
'access_user_management',
];
foreach ($permissions as $permission) {
@ -35,7 +36,6 @@ class PermissionsTableSeeder extends Seeder
]);
}
//Assign all the permissions to Admin role
$role = Role::create([
'name' => 'Admin'
]);

View File

@ -0,0 +1,68 @@
<?php
namespace Modules\User\Http\Controllers;
use Illuminate\Contracts\Support\Renderable;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
use Modules\Upload\Entities\Upload;
use Modules\User\Rules\MatchCurrentPassword;
class ProfileController extends Controller
{
public function edit() {
return view('user::profile');
}
public function update(Request $request) {
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email,' . auth()->id()
]);
auth()->user()->update([
'name' => $request->name,
'email' => $request->email
]);
if ($request->has('image')) {
if ($request->has('image')) {
$tempFile = Upload::where('folder', $request->image)->first();
if (auth()->user()->getFirstMedia('avatars')) {
auth()->user()->getFirstMedia('avatars')->delete();
}
if ($tempFile) {
auth()->user()->addMedia(Storage::path('public/temp/' . $request->image . '/' . $tempFile->filename))->toMediaCollection('avatars');
Storage::deleteDirectory('public/temp/' . $request->image);
$tempFile->delete();
}
}
}
toast('Profile Updated!', 'success');
return back();
}
public function updatePassword(Request $request) {
$request->validate([
'current_password' => ['required', 'max:255', new MatchCurrentPassword()],
'password' => 'required|min:8|max:255|confirmed'
]);
auth()->user()->update([
'password' => Hash::make($request->password)
]);
toast('Password Updated!', 'success');
return back();
}
}

View File

@ -6,33 +6,28 @@ use App\DataTables\RolesDataTable;
use Illuminate\Contracts\Support\Renderable;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Gate;
use Spatie\Permission\Models\Role;
class RolesController extends Controller
{
/**
* Display a listing of the resource.
* @return Renderable
*/
public function index(RolesDataTable $dataTable) {
abort_if(Gate::denies('access_user_management'), 403);
return $dataTable->render('user::roles.index');
}
/**
* Show the form for creating a new resource.
* @return Renderable
*/
public function create() {
abort_if(Gate::denies('access_user_management'), 403);
return view('user::roles.create');
}
/**
* Store a newly created resource in storage.
* @param Request $request
* @return Renderable
*/
public function store(Request $request) {
abort_if(Gate::denies('access_user_management'), 403);
$request->validate([
'name' => 'required|string|max:255',
'permissions' => 'required|array'
@ -49,22 +44,17 @@ class RolesController extends Controller
return redirect()->route('roles.index');
}
/**
* Show the form for editing the specified resource.
* @param int $id
* @return Renderable
*/
public function edit(Role $role) {
abort_if(Gate::denies('access_user_management'), 403);
return view('user::roles.edit', compact('role'));
}
/**
* Update the specified resource in storage.
* @param Request $request
* @param int $id
* @return Renderable
*/
public function update(Request $request, Role $role) {
abort_if(Gate::denies('access_user_management'), 403);
$request->validate([
'name' => 'required|string|max:255',
'permissions' => 'required|array'
@ -81,12 +71,10 @@ class RolesController extends Controller
return redirect()->route('roles.index');
}
/**
* Remove the specified resource from storage.
* @param int $id
* @return Renderable
*/
public function destroy(Role $role) {
abort_if(Gate::denies('access_user_management'), 403);
$role->delete();
toast('Role Deleted!', 'success');

View File

@ -2,73 +2,118 @@
namespace Modules\User\Http\Controllers;
use App\DataTables\UsersDataTable;
use App\Models\User;
use Illuminate\Contracts\Support\Renderable;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
use Modules\Upload\Entities\Upload;
class UsersController extends Controller
{
public function index(UsersDataTable $dataTable) {
abort_if(Gate::denies('access_user_management'), 403);
/**
* Display a listing of the resource.
* @return Renderable
*/
public function index() {
return view('user::users.index');
return $dataTable->render('user::users.index');
}
/**
* Show the form for creating a new resource.
* @return Renderable
*/
public function create() {
abort_if(Gate::denies('access_user_management'), 403);
return view('user::users.create');
}
/**
* Store a newly created resource in storage.
* @param Request $request
* @return Renderable
*/
public function store(Request $request) {
//
abort_if(Gate::denies('access_user_management'), 403);
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|max:255|unique:users,email',
'password' => 'required|string|min:8|max:255|confirmed'
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
'is_active' => $request->is_active
]);
$user->assignRole($request->role);
if ($request->has('image')) {
$tempFile = Upload::where('folder', $request->image)->first();
if ($tempFile) {
$user->addMedia(Storage::path('public/temp/' . $request->image . '/' . $tempFile->filename))->toMediaCollection('avatars');
Storage::deleteDirectory('public/temp/' . $request->image);
$tempFile->delete();
}
}
/**
* Show the specified resource.
* @param int $id
* @return Renderable
*/
public function show(User $user) {
return view('user::users.show', compact('user'));
toast("User Created & Assigned '$request->role' Role!", 'success');
return redirect()->route('users.index');
}
/**
* Show the form for editing the specified resource.
* @param int $id
* @return Renderable
*/
public function edit(User $user) {
abort_if(Gate::denies('access_user_management'), 403);
return view('user::users.edit', compact('user'));
}
/**
* Update the specified resource in storage.
* @param Request $request
* @param int $id
* @return Renderable
*/
public function update(Request $request, User $user) {
//
abort_if(Gate::denies('access_user_management'), 403);
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|max:255|unique:users,email,'.$user->id,
]);
$user->update([
'name' => $request->name,
'email' => $request->email,
'is_active' => $request->is_active
]);
$user->syncRoles($request->role);
if ($request->has('image')) {
$tempFile = Upload::where('folder', $request->image)->first();
if ($user->getFirstMedia('avatars')) {
$user->getFirstMedia('avatars')->delete();
}
/**
* Remove the specified resource from storage.
* @param int $id
* @return Renderable
*/
if ($tempFile) {
$user->addMedia(Storage::path('public/temp/' . $request->image . '/' . $tempFile->filename))->toMediaCollection('avatars');
Storage::deleteDirectory('public/temp/' . $request->image);
$tempFile->delete();
}
}
toast("User Updated & Assigned '$request->role' Role!", 'info');
return redirect()->route('users.index');
}
public function destroy(User $user) {
//
abort_if(Gate::denies('access_user_management'), 403);
$user->delete();
toast('User Deleted!', 'warning');
return redirect()->route('users.index');
}
}

View File

@ -0,0 +1,100 @@
@extends('layouts.app')
@section('title', 'User Profile')
@section('third_party_stylesheets')
@include('includes.filepond-css')
@endsection
@section('breadcrumb')
<ol class="breadcrumb border-0 m-0">
<li class="breadcrumb-item"><a href="{{ route('home') }}">Home</a></li>
<li class="breadcrumb-item active">Profile</li>
</ol>
@endsection
@section('content')
<div class="container-fluid">
<div class="row">
<div class="col-12">
@include('utils.alerts')
<h3>Hello, <span class="text-primary">{{ auth()->user()->name }}</span></h3>
<p class="font-italic">Change your profile information & password from here...</p>
</div>
<div class="col-lg-6">
<div class="card">
<div class="card-body">
<form action="{{ route('profile.update') }}" method="POST">
@csrf
@method('patch')
<div class="form-group">
<label for="image">Profile Image <span class="text-danger">*</span></label>
<img style="width: 100px;height: 100px;" class="d-block mx-auto img-thumbnail img-fluid rounded-circle mb-2" src="{{ auth()->user()->getFirstMediaUrl('avatars') }}" alt="Profile Image">
<input id="image" type="file" name="image" data-max-file-size="500KB">
</div>
<div class="form-group">
<label for="name">Name <span class="text-danger">*</span></label>
<input class="form-control" type="text" name="name" required value="{{ auth()->user()->name }}">
@error('name')
<p class="text-danger">{{ $message }}</p>
@enderror
</div>
<div class="form-group">
<label for="email">Email <span class="text-danger">*</span></label>
<input class="form-control" type="email" name="email" required value="{{ auth()->user()->email }}">
@error('email')
<p class="text-danger">{{ $message }}</p>
@enderror
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Update Profile <i class="bi bi-check"></i></button>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card">
<div class="card-body">
<form action="{{ route('profile.update.password') }}" method="POST">
@csrf
@method('patch')
<div class="form-group">
<label for="current_password">Current Password <span class="text-danger">*</span></label>
<input type="password" class="form-control" name="current_password" required>
@error('current_password')
<p class="text-danger">{{ $message }}</p>
@enderror
</div>
<div class="form-group">
<label for="password">New Password <span class="text-danger">*</span></label>
<input class="form-control" type="password" name="password" required>
@error('password')
<p class="text-danger">{{ $message }}</p>
@enderror
</div>
<div class="form-group">
<label for="password_confirmation">Confirm Password <span class="text-danger">*</span></label>
<input class="form-control" type="password" name="password_confirmation" required>
@error('password_confirmation')
<p class="text-danger">{{ $message }}</p>
@enderror
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Update Password <i class="bi bi-check"></i></button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('page_scripts')
@include('includes.filepond-js')
@endpush

View File

@ -14,6 +14,7 @@
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
@include('utils.alerts')
<form action="{{ route('roles.store') }}" method="POST">
@csrf
<div class="form-group">

View File

@ -22,6 +22,7 @@
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
@include('utils.alerts')
<form action="{{ route('roles.update', $role->id) }}" method="POST">
@csrf
@method('patch')

View File

@ -0,0 +1,130 @@
@extends('layouts.app')
@section('title', 'Create User')
@section('third_party_stylesheets')
<link href="https://unpkg.com/filepond/dist/filepond.css" rel="stylesheet"/>
<link href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css"
rel="stylesheet">
@endsection
@section('breadcrumb')
<ol class="breadcrumb border-0 m-0">
<li class="breadcrumb-item"><a href="{{ route('home') }}">Home</a></li>
<li class="breadcrumb-item"><a href="{{ route('users.index') }}">Users</a></li>
<li class="breadcrumb-item active">Create</li>
</ol>
@endsection
@section('content')
<div class="container-fluid mb-4">
<form action="{{ route('users.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<div class="row">
<div class="col-lg-12">
@include('utils.alerts')
<div class="form-group">
<button class="btn btn-primary">Create User <i class="bi bi-check"></i></button>
</div>
</div>
<div class="col-md-8">
<div class="card">
<div class="card-body">
<div class="form-row">
<div class="col-lg-6">
<div class="form-group">
<label for="name">Name <span class="text-danger">*</span></label>
<input class="form-control" type="text" name="name" required>
</div>
</div>
<div class="col-lg-6">
<div class="form-group">
<label for="email">Email <span class="text-danger">*</span></label>
<input class="form-control" type="email" name="email" required>
</div>
</div>
</div>
<div class="form-row">
<div class="col-lg-6">
<div class="form-group">
<label for="password">Password <span class="text-danger">*</span></label>
<input class="form-control" type="password" name="password" required>
</div>
</div>
<div class="col-lg-6">
<div class="form-group">
<label for="password_confirmation">Password <span
class="text-danger">*</span></label>
<input class="form-control" type="password" name="password_confirmation"
required>
</div>
</div>
</div>
<div class="form-group">
<label for="role">Role <span class="text-danger">*</span></label>
<select class="form-control" name="role" id="role" required>
<option value="" selected disabled>Select Role</option>
@foreach(\Spatie\Permission\Models\Role::where('name', '!=', 'Super Admin')->get() as $role)
<option value="{{ $role->name }}">{{ $role->name }}</option>
@endforeach
</select>
</div>
<div class="form-group">
<label for="is_active">Status <span class="text-danger">*</span></label>
<select class="form-control" name="is_active" id="is_active" required>
<option value="" selected disabled>Select Status</option>
<option value="1">Active</option>
<option value="2">Deactive</option>
</select>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-body">
<div class="form-group">
<label for="image">Profile Image <span class="text-danger">*</span></label>
<input id="image" type="file" name="image" data-max-file-size="500KB">
</div>
</div>
</div>
</div>
</div>
</form>
</div>
@endsection
@section('third_party_scripts')
<script src="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.js"></script>
<script
src="https://unpkg.com/filepond-plugin-file-validate-size/dist/filepond-plugin-file-validate-size.js"></script>
<script
src="https://unpkg.com/filepond-plugin-file-validate-type/dist/filepond-plugin-file-validate-type.js"></script>
<script src="https://unpkg.com/filepond/dist/filepond.js"></script>
@endsection
@push('page_scripts')
<script>
FilePond.registerPlugin(
FilePondPluginImagePreview,
FilePondPluginFileValidateSize,
FilePondPluginFileValidateType
);
const fileElement = document.querySelector('input[id="image"]');
const pond = FilePond.create(fileElement, {
acceptedFileTypes: ['image/png', 'image/jpg', 'image/jpeg'],
});
FilePond.setOptions({
server: {
url: "{{ route('filepond.upload') }}",
headers: {
"X-CSRF-TOKEN": "{{ csrf_token() }}"
}
}
});
</script>
@endpush

View File

@ -0,0 +1,115 @@
@extends('layouts.app')
@section('title', 'Edit User')
@section('third_party_stylesheets')
<link href="https://unpkg.com/filepond/dist/filepond.css" rel="stylesheet"/>
<link href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css"
rel="stylesheet">
@endsection
@section('breadcrumb')
<ol class="breadcrumb border-0 m-0">
<li class="breadcrumb-item"><a href="{{ route('home') }}">Home</a></li>
<li class="breadcrumb-item"><a href="{{ route('users.index') }}">Users</a></li>
<li class="breadcrumb-item active">Edit</li>
</ol>
@endsection
@section('content')
<div class="container-fluid mb-4">
<form action="{{ route('users.update', $user->id) }}" method="POST" enctype="multipart/form-data">
@csrf
@method('patch')
<div class="row">
<div class="col-lg-12">
@include('utils.alerts')
<div class="form-group">
<button class="btn btn-primary">Update User <i class="bi bi-check"></i></button>
</div>
</div>
<div class="col-md-8">
<div class="card">
<div class="card-body">
<div class="form-row">
<div class="col-lg-6">
<div class="form-group">
<label for="name">Name <span class="text-danger">*</span></label>
<input class="form-control" type="text" name="name" required value="{{ $user->name }}">
</div>
</div>
<div class="col-lg-6">
<div class="form-group">
<label for="email">Email <span class="text-danger">*</span></label>
<input class="form-control" type="email" name="email" required value="{{ $user->email }}">
</div>
</div>
</div>
<div class="form-group">
<label for="role">Role <span class="text-danger">*</span></label>
<select class="form-control" name="role" id="role" required>
@foreach(\Spatie\Permission\Models\Role::where('name', '!=', 'Super Admin')->get() as $role)
<option {{ $user->hasRole($role->name) ? 'selected' : '' }} value="{{ $role->name }}">{{ $role->name }}</option>
@endforeach
</select>
</div>
<div class="form-group">
<label for="is_active">Status <span class="text-danger">*</span></label>
<select class="form-control" name="is_active" id="is_active" required>
<option value="1" {{ $user->is_active == 1 ? 'selected' : ''}}>Active</option>
<option value="2" {{ $user->is_active == 2 ? 'selected' : ''}}>Deactive</option>
</select>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-body">
<div class="form-group">
<label for="image">Profile Image <span class="text-danger">*</span></label>
<img style="width: 100px;height: 100px;" class="d-block mx-auto img-thumbnail img-fluid rounded-circle mb-2" src="{{ $user->getFirstMediaUrl('avatars') }}" alt="Profile Image">
<input id="image" type="file" name="image" data-max-file-size="500KB">
</div>
</div>
</div>
</div>
</div>
</form>
</div>
@endsection
@section('third_party_scripts')
<script src="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.js"></script>
<script
src="https://unpkg.com/filepond-plugin-file-validate-size/dist/filepond-plugin-file-validate-size.js"></script>
<script
src="https://unpkg.com/filepond-plugin-file-validate-type/dist/filepond-plugin-file-validate-type.js"></script>
<script src="https://unpkg.com/filepond/dist/filepond.js"></script>
@endsection
@push('page_scripts')
<script>
FilePond.registerPlugin(
FilePondPluginImagePreview,
FilePondPluginFileValidateSize,
FilePondPluginFileValidateType
);
const fileElement = document.querySelector('input[id="image"]');
const pond = FilePond.create(fileElement, {
acceptedFileTypes: ['image/png', 'image/jpg', 'image/jpeg'],
});
FilePond.setOptions({
server: {
url: "{{ route('filepond.upload') }}",
headers: {
"X-CSRF-TOKEN": "{{ csrf_token() }}"
}
}
});
</script>
@endpush

View File

@ -0,0 +1,41 @@
@extends('layouts.app')
@section('title', 'Users')
@section('third_party_stylesheets')
<link rel="stylesheet" href="https://cdn.datatables.net/1.10.25/css/dataTables.bootstrap4.min.css">
@endsection
@section('breadcrumb')
<ol class="breadcrumb border-0 m-0">
<li class="breadcrumb-item"><a href="{{ route('home') }}">Home</a></li>
<li class="breadcrumb-item active">Users</li>
</ol>
@endsection
@section('content')
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body">
<!-- Button trigger modal -->
<a href="{{ route('users.create') }}" class="btn btn-primary">
Add User <i class="bi bi-plus"></i>
</a>
<hr>
<div class="table-responsive">
{!! $dataTable->table() !!}
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('page_scripts')
{!! $dataTable->scripts() !!}
@endpush

View File

@ -0,0 +1,15 @@
<a href="{{ route('users.edit', $data->id) }}" class="btn btn-info btn-sm">
<i class="bi bi-pencil"></i>
</a>
<button id="delete" class="btn btn-danger btn-sm" onclick="
event.preventDefault();
if (confirm('Are you sure? It will delete the data permanently!')) {
document.getElementById('destroy{{ $data->id }}').submit();
}
">
<i class="bi bi-trash"></i>
<form id="destroy{{ $data->id }}" class="d-none" action="{{ route('users.destroy', $data->id) }}" method="POST">
@csrf
@method('delete')
</form>
</button>

View File

@ -0,0 +1,3 @@
@foreach($roles as $role)
<span class="badge badge-primary">{{ $role }}</span>
@endforeach

View File

@ -8,8 +8,12 @@
Route::group(['middleware' => 'auth'], function () {
//User Profile
Route::get('/user/profile', 'ProfileController@edit')->name('profile.edit');
Route::patch('/user/profile', 'ProfileController@update')->name('profile.update');
Route::patch('/user/password', 'ProfileController@updatePassword')->name('profile.update.password');
//Users
Route::resource('users', 'UsersController');
Route::resource('users', 'UsersController')->except('show');
//Roles
Route::resource('roles', 'RolesController')->except('show');

View File

@ -0,0 +1,41 @@
<?php
namespace Modules\User\Rules;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Support\Facades\Hash;
class MatchCurrentPassword implements Rule
{
/**
* Create a new rule instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
return Hash::check($value, auth()->user()->password);
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return __('validation.current_password');
}
}

View File

@ -42,7 +42,7 @@ class RolesDataTable extends DataTable
{
return $model->newQuery()->with(['permissions' => function($query) {
$query->select('name')->get();
}]);
}])->where('name', '!=', 'Super Admin');
}
/**

View File

@ -21,9 +21,27 @@ class UsersDataTable extends DataTable
{
return datatables()
->eloquent($query)
->addColumn('role', function ($data) {
return view('user::users.partials.roles', [
'roles' => $data->getRoleNames()
]);
})
->addColumn('action', function ($data) {
return view('user::users.partials.actions', compact('data'));
});
})
->addColumn('status', function ($data) {
if ($data->is_active == 1) {
$html = '<span class="badge badge-success">Active</span>';
} else {
$html = '<span class="badge badge-warning">Deactivated</span>';
}
return $html;
})
->addColumn('image', function ($data) {
$url = $data->getFirstMediaUrl('avatars');
return '<img src="'.$url.'" style="width:50px;height:50px;" class="img-thumbnail rounded-circle"/>';
})
->rawColumns(['image', 'status']);
}
/**
@ -34,7 +52,11 @@ class UsersDataTable extends DataTable
*/
public function query(User $model)
{
return $model->newQuery();
return $model->newQuery()
->with(['roles' => function ($query) {
$query->select('name')->get();
}])
->where('id', '!=', auth()->id());
}
/**
@ -72,13 +94,25 @@ class UsersDataTable extends DataTable
protected function getColumns()
{
return [
Column::make('id'),
Column::make('created_at'),
Column::make('updated_at'),
Column::computed('image')
->className('text-center align-middle'),
Column::make('name')
->className('text-center align-middle'),
Column::make('email')
->className('text-center align-middle'),
Column::computed('role')
->className('text-center align-middle'),
Column::computed('status')
->className('text-center align-middle'),
Column::computed('action')
->exportable(false)
->printable(false)
->addClass('text-center'),
->className('text-center align-middle'),
];
}

View File

@ -5,6 +5,8 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
@ -37,4 +39,16 @@ class LoginController extends Controller
{
$this->middleware('guest')->except('logout');
}
protected function authenticated(Request $request, $user) {
if ($user->is_active != 1) {
Auth::logout();
return back()->with([
'account_deactivated' => 'Your account is deactivated! Please contact with Super Admin.'
]);
}
return next($request);
}
}

View File

@ -64,10 +64,15 @@ class RegisterController extends Controller
*/
protected function create(array $data)
{
return User::create([
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
'is_active' => 1
]);
$user->assignRole('Admin');
return $user;
}
}

View File

@ -3,13 +3,18 @@
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\File;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
class User extends Authenticatable implements HasMedia
{
use HasFactory, Notifiable;
use HasFactory, Notifiable, HasRoles, InteractsWithMedia;
/**
* The attributes that are mass assignable.
@ -20,6 +25,7 @@ class User extends Authenticatable
'name',
'email',
'password',
'is_active'
];
/**
@ -40,4 +46,16 @@ class User extends Authenticatable
protected $casts = [
'email_verified_at' => 'datetime',
];
protected $with = ['media'];
public function registerMediaCollections(): void
{
$this->addMediaCollection('avatars')
->useFallbackUrl('/images/fallback_profile_image.png');
}
public function scopeIsActive(Builder $builder) {
return $builder->where('is_active', 1);
}
}

View File

@ -25,6 +25,8 @@ class AuthServiceProvider extends ServiceProvider
{
$this->registerPolicies();
//
Gate::before(function ($user, $ability) {
return $user->hasRole('Super Admin') ? true : null;
});
}
}

View File

@ -0,0 +1,33 @@
<?php
return [
/*
* DataTables JavaScript global namespace.
*/
'namespace' => 'LaravelDataTables',
/*
* Default table attributes when generating the table.
*/
'table' => [
'class' => 'table table-bordered',
'id' => 'dataTableBuilder',
],
/*
* Default condition to determine if a parameter is a callback or not.
* Callbacks needs to start by those terms or they will be casted to string.
*/
'callback' => ['$', '$.', 'function'],
/*
* Html builder script template.
*/
'script' => 'datatables::script',
/*
* Html builder script template for DataTables Editor integration.
*/
'editor' => 'datatables::editor',
];

122
config/datatables.php Normal file
View File

@ -0,0 +1,122 @@
<?php
return [
/*
* DataTables search options.
*/
'search' => [
/*
* Smart search will enclose search keyword with wildcard string "%keyword%".
* SQL: column LIKE "%keyword%"
*/
'smart' => true,
/*
* Multi-term search will explode search keyword using spaces resulting into multiple term search.
*/
'multi_term' => true,
/*
* Case insensitive will search the keyword in lower case format.
* SQL: LOWER(column) LIKE LOWER(keyword)
*/
'case_insensitive' => true,
/*
* Wild card will add "%" in between every characters of the keyword.
* SQL: column LIKE "%k%e%y%w%o%r%d%"
*/
'use_wildcards' => false,
/*
* Perform a search which starts with the given keyword.
* SQL: column LIKE "keyword%"
*/
'starts_with' => false,
],
/*
* DataTables internal index id response column name.
*/
'index_column' => 'DT_RowIndex',
/*
* List of available builders for DataTables.
* This is where you can register your custom dataTables builder.
*/
'engines' => [
'eloquent' => Yajra\DataTables\EloquentDataTable::class,
'query' => Yajra\DataTables\QueryDataTable::class,
'collection' => Yajra\DataTables\CollectionDataTable::class,
'resource' => Yajra\DataTables\ApiResourceDataTable::class,
],
/*
* DataTables accepted builder to engine mapping.
* This is where you can override which engine a builder should use
* Note, only change this if you know what you are doing!
*/
'builders' => [
//Illuminate\Database\Eloquent\Relations\Relation::class => 'eloquent',
//Illuminate\Database\Eloquent\Builder::class => 'eloquent',
//Illuminate\Database\Query\Builder::class => 'query',
//Illuminate\Support\Collection::class => 'collection',
],
/*
* Nulls last sql pattern for PostgreSQL & Oracle.
* For MySQL, use 'CASE WHEN :column IS NULL THEN 1 ELSE 0 END, :column :direction'
*/
'nulls_last_sql' => ':column :direction NULLS LAST',
/*
* User friendly message to be displayed on user if error occurs.
* Possible values:
* null - The exception message will be used on error response.
* 'throw' - Throws a \Yajra\DataTables\Exceptions\Exception. Use your custom error handler if needed.
* 'custom message' - Any friendly message to be displayed to the user. You can also use translation key.
*/
'error' => env('DATATABLES_ERROR', null),
/*
* Default columns definition of dataTable utility functions.
*/
'columns' => [
/*
* List of columns hidden/removed on json response.
*/
'excess' => ['rn', 'row_num'],
/*
* List of columns to be escaped. If set to *, all columns are escape.
* Note: You can set the value to empty array to disable XSS protection.
*/
'escape' => '*',
/*
* List of columns that are allowed to display html content.
* Note: Adding columns to list will make us available to XSS attacks.
*/
'raw' => ['action'],
/*
* List of columns are are forbidden from being searched/sorted.
*/
'blacklist' => ['password', 'remember_token'],
/*
* List of columns that are only allowed fo search/sort.
* If set to *, all columns are allowed.
*/
'whitelist' => '*',
],
/*
* JsonResponse header and options config.
*/
'json' => [
'header' => [],
'options' => 0,
],
];

View File

@ -19,6 +19,7 @@ class CreateUsersTable extends Migration
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->boolean('is_active');
$table->rememberToken();
$table->timestamps();
});

View File

@ -3,6 +3,7 @@
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Modules\User\Database\Seeders\PermissionsTableSeeder;
class DatabaseSeeder extends Seeder
{
@ -13,6 +14,7 @@ class DatabaseSeeder extends Seeder
*/
public function run()
{
// \App\Models\User::factory(10)->create();
$this->call(PermissionsTableSeeder::class);
$this->call(SuperUserSeeder::class);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
use Spatie\Permission\Models\Role;
class SuperUserSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
$user = User::create([
'name' => 'Administrator',
'email' => 'admin@gmail.com',
'password' => Hash::make(12345678)
]);
$superAdmin = Role::create([
'name' => 'Super Admin'
]);
$user->assignRole($superAdmin);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

6
public/js/filepond-upload.js vendored Normal file
View File

@ -0,0 +1,6 @@
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/
/******/
/******/ })()
;

View File

@ -11,18 +11,19 @@
<!-- CoreUI CSS -->
<link rel="stylesheet" href="{{ mix('css/app.css') }}" crossorigin="anonymous">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css"
integrity="sha512-1PKOgIY59xJ8Co8+NE6FZ+LOAZKjy+KY8iq0G4B3CyeY6wYHN3yt9PW0XpSriVlkMXe40PTKnXrLnZ9+fkDaog=="
crossorigin="anonymous"/>
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css">
</head>
<body class="c-app flex-row align-items-center">
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
@if(Session::has('account_deactivated'))
<div class="alert alert-danger" role="alert">
{{ Session::get('account_deactivated') }}
</div>
@endif
<div class="card-group">
<div class="card p-4">
<div class="card-body">
@ -33,7 +34,7 @@
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text">
<i class="cil-user"></i>
<i class="bi bi-person"></i>
</span>
</div>
<input type="email" class="form-control @error('email') is-invalid @enderror"
@ -46,7 +47,7 @@
<div class="input-group mb-4">
<div class="input-group-prepend">
<span class="input-group-text">
<i class="cil-lock-locked"></i>
<i class="bi bi-lock"></i>
</span>
</div>
<input type="password"

View File

@ -11,11 +11,8 @@
<!-- CoreUI CSS -->
<link rel="stylesheet" href="{{ mix('css/app.css') }}" crossorigin="anonymous">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css"
integrity="sha512-1PKOgIY59xJ8Co8+NE6FZ+LOAZKjy+KY8iq0G4B3CyeY6wYHN3yt9PW0XpSriVlkMXe40PTKnXrLnZ9+fkDaog=="
crossorigin="anonymous"/>
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css">
</head>
<body class="c-app flex-row align-items-center">
<div class="container">
@ -30,7 +27,7 @@
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text">
<i class="cil-user"></i>
<i class="bi bi-person"></i>
</span>
</div>
<input type="text" class="form-control @error('name') is-invalid @enderror"
@ -43,7 +40,7 @@
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text">
<i class="cil-envelope-open"></i>
<i class="bi bi-envelope"></i>
</span>
</div>
<input type="email" class="form-control @error('email') is-invalid @enderror"
@ -55,7 +52,7 @@
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text">
<i class="cil-lock-locked"></i>
<i class="bi bi-lock"></i>
</span>
</div>
<input type="password" class="form-control @error('password') is-invalid @enderror"
@ -67,14 +64,14 @@
<div class="input-group mb-4">
<div class="input-group-prepend">
<span class="input-group-text">
<i class="cil-lock-locked"></i>
<i class="bi bi-lock"></i>
</span>
</div>
<input type="password" name="password_confirmation" class="form-control"
placeholder="Confirm password">
</div>
<button type="submit" class="btn btn-primary btn-block btn-flat">Register</button>
<a href="{{ route('login') }}" class="text-center">I already have a membership</a>
<button type="submit" class="btn btn-primary btn-block btn-flat mb-3">Register</button>
<a href="{{ route('login') }}" class="text-center">I already have a membership.</a>
</form>
</div>
</div>

View File

@ -0,0 +1,2 @@
<link href="https://unpkg.com/filepond/dist/filepond.css" rel="stylesheet"/>
<link href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css" rel="stylesheet">

View File

@ -0,0 +1,25 @@
<script src="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.js"></script>
<script src="https://unpkg.com/filepond-plugin-file-validate-size/dist/filepond-plugin-file-validate-size.js"></script>
<script src="https://unpkg.com/filepond-plugin-file-validate-type/dist/filepond-plugin-file-validate-type.js"></script>
<script src="https://unpkg.com/filepond/dist/filepond.js"></script>
<script>
FilePond.registerPlugin(
FilePondPluginImagePreview,
FilePondPluginFileValidateSize,
FilePondPluginFileValidateType
);
const fileElement = document.querySelector('input[id="image"]');
const pond = FilePond.create(fileElement, {
acceptedFileTypes: ['image/png', 'image/jpg', 'image/jpeg'],
});
FilePond.setOptions({
server: {
process: "{{ route('filepond.upload') }}",
revert: "{{ route('filepond.delete') }}",
headers: {
"X-CSRF-TOKEN": "{{ csrf_token() }}"
}
}
});
</script>

View File

@ -1,4 +1,4 @@
<footer class="c-footer">
<div><a href="#">Triangle POS</a> © {{ date('Y') }} Developed by <strong>Fahim Anzam</strong>.</div>
<div><a href="#">Triangle POS</a> © {{ date('Y') }} Developed by <strong><a target="_blank" href="https://fahim.codes">Fahim Anzam</a></strong>.</div>
<div class="mfs-auto d-md-down-none">Powered by&nbsp;<a href="https://coreui.io/">CoreUI</a></div>
</footer>

View File

@ -21,12 +21,12 @@
aria-haspopup="true" aria-expanded="false">
<span class="mr-2 font-weight-bold">{{ auth()->user()->name }}</span>
<div class="c-avatar">
<img class="c-avatar-img border border-primary" src="https://www.gravatar.com/avatar/00000000000000000000000000000000" alt="">
<img class="c-avatar rounded-circle" src="{{ auth()->user()->getFirstMediaUrl('avatars') }}" alt="Profile Image">
</div>
</a>
<div class="dropdown-menu dropdown-menu-right pt-0">
<div class="dropdown-header bg-light py-2"><strong>Account</strong></div>
<a class="dropdown-item" href="#">
<a class="dropdown-item" href="{{ route('profile.edit') }}">
<i class="mfe-2 bi bi-person" style="font-size: 1.2rem;"></i> Profile
</a>
<a class="dropdown-item" href="#" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">

View File

@ -4,21 +4,26 @@
</a>
</li>
@can('access_products')
<li class="c-sidebar-nav-item c-sidebar-nav-dropdown {{ request()->routeIs('products.*') || request()->routeIs('product-categories.*') ? 'c-show' : '' }}">
<a class="c-sidebar-nav-link c-sidebar-nav-dropdown-toggle" href="#">
<i class="c-sidebar-nav-icon bi bi-journal-bookmark" style="line-height: 1;"></i> Products
</a>
<ul class="c-sidebar-nav-dropdown-items">
@can('access_product_categories')
<li class="c-sidebar-nav-item">
<a class="c-sidebar-nav-link {{ request()->routeIs('product-categories.*') ? 'c-active' : '' }}" href="{{ route('product-categories.index') }}">
<i class="c-sidebar-nav-icon bi bi-collection" style="line-height: 1;"></i> Categories
</a>
</li>
@endcan
@can('create_products')
<li class="c-sidebar-nav-item">
<a class="c-sidebar-nav-link {{ request()->routeIs('products.create') ? 'c-active' : '' }}" href="{{ route('products.create') }}">
<i class="c-sidebar-nav-icon bi bi-journal-plus" style="line-height: 1;"></i> Create Product
</a>
</li>
@endcan
<li class="c-sidebar-nav-item">
<a class="c-sidebar-nav-link {{ request()->routeIs('products.index') ? 'c-active' : '' }}" href="{{ route('products.index') }}">
<i class="c-sidebar-nav-icon bi bi-journals" style="line-height: 1;"></i> All Products
@ -26,7 +31,9 @@
</li>
</ul>
</li>
@endcan
@can('access_user_management')
<li class="c-sidebar-nav-item c-sidebar-nav-dropdown {{ request()->routeIs('roles*') ? 'c-show' : '' }}">
<a class="c-sidebar-nav-link c-sidebar-nav-dropdown-toggle" href="#">
<i class="c-sidebar-nav-icon bi bi-people" style="line-height: 1;"></i> User Management
@ -49,3 +56,4 @@
</li>
</ul>
</li>
@endcan