Added voucher system

This commit is contained in:
AVMG20 2021-07-10 00:18:22 +02:00
parent ed553674b2
commit a7f8d71f92
12 changed files with 504 additions and 191 deletions

View file

@ -0,0 +1,142 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Voucher;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class VoucherController extends Controller
{
/**
* Display a listing of the resource.
*
* @return Application|Factory|View
*/
public function index()
{
return view('admin.vouchers.index');
}
/**
* Show the form for creating a new resource.
*
* @return Application|Factory|View
*/
public function create()
{
return view('admin.vouchers.create');
}
/**
* Store a newly created resource in storage.
*
* @param Request $request
* @return RedirectResponse
*/
public function store(Request $request)
{
$request->validate([
'memo' => 'sometimes|string|max:191',
'code' => 'required|string|alpha_dash|max:36',
'uses' => 'required|numeric|max:2147483647',
'credits' => 'required|numeric|between:0,99999999',
'expires_at' => 'required|date|after:today',
]);
Voucher::create($request->except('_token'));
return redirect()->route('admin.vouchers.index')->with('success', 'voucher has been created!');
}
/**
* Display the specified resource.
*
* @param Voucher $voucher
* @return Response
*/
public function show(Voucher $voucher)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param Voucher $voucher
* @return Response
*/
public function edit(Voucher $voucher)
{
//
}
/**
* Update the specified resource in storage.
*
* @param Request $request
* @param Voucher $voucher
* @return Response
*/
public function update(Request $request, Voucher $voucher)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param Voucher $voucher
* @return RedirectResponse
*/
public function destroy(Voucher $voucher)
{
$voucher->delete();
return redirect()->back()->with('success', 'voucher has been removed!');
}
public function dataTable()
{
$query = Voucher::with(['users']);
return datatables($query)
->addColumn('actions', function (Voucher $voucher) {
return '
<a data-content="Show" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.vouchers.show', $voucher->id) . '" class="btn btn-sm text-white btn-warning mr-1"><i class="fas fa-eye"></i></a>
<a data-content="Edit" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.vouchers.edit', $voucher->id) . '" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></a>
<form class="d-inline" onsubmit="return submitResult();" method="post" action="' . route('admin.vouchers.destroy', $voucher->id) . '">
' . csrf_field() . '
' . method_field("DELETE") . '
<button data-content="Delete" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
</form>
';
})
->addColumn('status', function (Voucher $voucher) {
$color = 'success';
if ($voucher->getStatus() != 'VALID') $color = 'danger';
return '<span class="badge badge-'.$color.'">'. $voucher->getStatus() .'</span>';
})
->editColumn('uses', function (Voucher $voucher) {
$userCount = $voucher->users()->count();
return "{$userCount} / {$voucher->uses}";
})
->editColumn('credits', function (Voucher $voucher) {
return number_format($voucher->credits, 2, '.', '');
})
->editColumn('expires_at', function (Voucher $voucher) {
return $voucher->expires_at ? $voucher->expires_at->diffForHumans() : '';
})
->editColumn('code' , function (Voucher $voucher) {
return "<code>{$voucher->code}</code>";
})
->rawColumns(['actions' , 'code' , 'status'])
->make();
}
}

57
app/Models/Voucher.php Normal file
View file

@ -0,0 +1,57 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* Class Voucher
* @package App\Models
*/
class Voucher extends Model
{
use HasFactory;
/**
* @var string[]
*/
protected $fillable = [
'memo',
'code',
'credits',
'uses',
'expires_at',
];
protected $dates = [
'expires_at'
];
/**
*
*/
public static function boot()
{
parent::boot();
static::deleting(function (Voucher $voucher) {
$voucher->users()->detach();
});
}
public function getStatus(){
if ($this->users()->count() >= $this->uses) return 'USES_LIMIT_REACHED';
if ($this->expires_at->isPast()) return 'EXPIRED';
return 'VALID';
}
/**
* @return BelongsToMany
*/
public function users()
{
return $this->belongsToMany(User::class);
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Database\Factories;
use App\Models\voucher;
use Illuminate\Database\Eloquent\Factories\Factory;
class VoucherFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = voucher::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'credits' => $this->faker->numberBetween(100, 1000),
'expires_at' => $this->faker->dateTimeBetween(now(), '+30 days')
];
}
}

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateVouchersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('vouchers', function (Blueprint $table) {
$table->id();
$table->string('code', 36)->unique();
$table->string('memo')->nullable();
$table->unsignedFloat('credits', 10);
$table->unsignedInteger('uses')->default(1);
$table->timestamp('expires_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('vouchers');
}
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUserVoucherTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('user_voucher', function (Blueprint $table) {
$table->foreignId('user_id')->constrained();
$table->foreignId('voucher_id')->constrained();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('user_voucher');
}
}

View file

@ -72,7 +72,7 @@
<div class="form-group">
<label for="credits">Credits</label>
<input value="{{$user->credits}}" id="credits" name="credits" step="any" min="0"
max="1000000"
max="999999"
type="number" class="form-control @error('credits') is-invalid @enderror"
required="required">
@error('credits')

View file

@ -6,12 +6,12 @@
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1>Products</h1>
<h1>Vouchers</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{route('home')}}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{route('admin.products.index')}}">Products</a></li>
<li class="breadcrumb-item"><a href="{{route('admin.products.index')}}">Vouchers</a></li>
<li class="breadcrumb-item"><a class="text-muted" href="{{route('admin.products.create')}}">Create</a>
</li>
</ol>
@ -28,163 +28,94 @@
<div class="row">
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">
<i class="fas fa-money-check-alt mr-2"></i>Voucher details
</h5>
</div>
<div class="card-body">
<form action="{{route('admin.products.store')}}" method="POST">
<form action="{{route('admin.vouchers.store')}}" method="POST">
@csrf
<div class="d-flex flex-row-reverse">
<div class="custom-control custom-switch">
<input type="checkbox" name="disabled" class="custom-control-input custom-control-input-danger" id="switch1">
<label class="custom-control-label" for="switch1">Disabled <i data-toggle="popover" data-trigger="hover" data-content="Will hide this option from being selected" class="fas fa-info-circle"></i></label>
<div class="form-group">
<label for="memo">Memo</label>
<input value="{{old('memo')}}" placeholder="Summer break voucher" id="memo"
name="memo" type="text"
class="form-control @error('memo') is-invalid @enderror">
@error('memo')
<div class="text-danger">
{{$message}}
</div>
@enderror
</div>
<div class="row">
<div class="col-lg-6">
<div class="form-group">
<label for="name">Name</label>
<input value="{{old('name')}}" id="name" name="name" type="text"
class="form-control @error('name') is-invalid @enderror"
required="required">
@error('name')
<div class="invalid-feedback">
{{$message}}
</div>
@enderror
</div>
<div class="form-group">
<label for="price">Price in credits</label>
<input value="{{old('price')}}" id="price" name="price"
type="number"
class="form-control @error('price') is-invalid @enderror"
required="required">
@error('price')
<div class="invalid-feedback">
{{$message}}
</div>
@enderror
</div>
<div class="form-group">
<label for="memory">Memory</label>
<input value="{{old('memory')}}" id="memory" name="memory"
type="number"
class="form-control @error('memory') is-invalid @enderror"
required="required">
@error('memory')
<div class="invalid-feedback">
{{$message}}
</div>
@enderror
</div>
<div class="form-group">
<label for="cpu">Cpu</label>
<input value="{{old('cpu')}}" id="cpu" name="cpu"
type="number"
class="form-control @error('cpu') is-invalid @enderror"
required="required">
@error('cpu')
<div class="invalid-feedback">
{{$message}}
</div>
@enderror
</div>
<div class="form-group">
<label for="swap">Swap</label>
<input value="{{old('swap')}}" id="swap" name="swap"
type="number"
class="form-control @error('swap') is-invalid @enderror"
required="required">
@error('swap')
<div class="invalid-feedback">
{{$message}}
</div>
@enderror
</div>
<div class="form-group">
<label for="description">Description <i data-toggle="popover" data-trigger="hover" data-content="This is what the users sees" class="fas fa-info-circle"></i></label>
<textarea id="description" name="description"
type="text"
class="form-control @error('description') is-invalid @enderror"
required="required">{{old('description')}}</textarea>
@error('description')
<div class="invalid-feedback">
{{$message}}
</div>
@enderror
</div>
</div>
<div class="col-lg-6">
<div class="form-group">
<label for="disk">Disk</label>
<input value="{{old('disk') ?? 1000}}" id="disk" name="disk"
type="number"
class="form-control @error('disk') is-invalid @enderror"
required="required">
@error('disk')
<div class="invalid-feedback">
{{$message}}
</div>
@enderror
</div>
<div class="form-group">
<label for="io">IO</label>
<input value="{{old('io') ?? 500}}" id="io" name="io"
type="number"
class="form-control @error('io') is-invalid @enderror"
required="required">
@error('io')
<div class="invalid-feedback">
{{$message}}
</div>
@enderror
</div>
<div class="form-group">
<label for="databases">Databases</label>
<input value="{{old('databases') ?? 1}}" id="databases"
name="databases"
type="number"
class="form-control @error('databases') is-invalid @enderror"
required="required">
@error('databases')
<div class="invalid-feedback">
{{$message}}
</div>
@enderror
</div>
<div class="form-group">
<label for="backups">Backups</label>
<input value="{{old('backups') ?? 1}}" id="backups"
name="backups"
type="number"
class="form-control @error('backups') is-invalid @enderror"
required="required">
@error('backups')
<div class="invalid-feedback">
{{$message}}
</div>
@enderror
</div>
<div class="form-group">
<label for="allocations">Allocations</label>
<input value="{{old('allocations') ?? 0}}"
id="allocations" name="allocations"
type="number"
class="form-control @error('allocations') is-invalid @enderror"
required="required">
@error('allocations')
<div class="invalid-feedback">
{{$message}}
</div>
@enderror
</div>
<div class="form-group">
<label for="credits">Credits *</label>
<input value="{{old('credits')}}" placeholder="500" id="credits"
name="credits" type="number" step="any" min="0"
max="999999"
class="form-control @error('credits') is-invalid @enderror">
@error('credits')
<div class="text-danger">
{{$message}}
</div>
@enderror
</div>
<div class="form-group">
<label for="code">Code *</label>
<div class="input-group">
<input value="{{old('code')}}" placeholder="SUMMER" id="code" name="code"
type="text"
class="form-control @error('code') is-invalid @enderror"
required="required">
<div class="input-group-append">
<button class="btn btn-info" onclick="setRandomCode()" type="button">
Random
</button>
</div>
</div>
@error('code')
<div class="text-danger">
{{$message}}
</div>
@enderror
</div>
<div class="form-group">
<label for="uses">Uses *</label>
<div class="input-group">
<input value="{{old('uses') ?? 1}}" id="uses" min="1" max="2147483647"
name="uses" type="number"
class="form-control @error('uses') is-invalid @enderror"
required="required">
<div class="input-group-append">
<button class="btn btn-info" onclick="setMaxUses()" type="button">Max
</button>
</div>
</div>
@error('uses')
<div class="text-danger">
{{$message}}
</div>
@enderror
</div>
<div class="form-group">
<label for="expires_at">Expires at</label>
<input value="{{old('expires_at')}}" id="expires_at" name="expires_at"
type="datetime-local"
class="form-control @error('expires_at') is-invalid @enderror">
@error('expires_at')
<div class="text-danger">
{{$message}}
</div>
@enderror
</div>
<div class="form-group text-right">
<button type="submit" class="btn btn-primary">
Submit
@ -201,5 +132,30 @@
<!-- END CONTENT -->
<script>
function setMaxUses() {
let element = document.getElementById('uses')
element.value = element.max;
console.log(element.max)
}
function setRandomCode() {
let element = document.getElementById('code')
element.value = getRandomCode(36)
}
function getRandomCode(length) {
let result = '';
let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-';
let charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() *
charactersLength));
}
return result;
}
</script>
@endsection

View file

@ -12,7 +12,7 @@
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{route('home')}}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{route('admin.products.index')}}">Vouchers</a></li>
<li class="breadcrumb-item"><a class="text-muted" href="{{route('admin.products.create')}}">Create</a>
<li class="breadcrumb-item"><a class="text-muted" href="{{route('admin.products.edit' , $voucher->id)}}">Edit</a>
</li>
</ol>
</div>
@ -34,12 +34,13 @@
</h5>
</div>
<div class="card-body">
<form action="{{route('admin.vouchers.store')}}" method="POST">
<form action="{{route('admin.vouchers.update' , $voucher->id)}}" method="POST">
@csrf
@method('PATCH')
<div class="form-group">
<label for="memo">Memo</label>
<input value="{{old('memo')}}" placeholder="Summer break voucher" id="memo"
<input value="{{ $voucher->memo }}" placeholder="Summer break voucher" id="memo"
name="memo" type="text"
class="form-control @error('memo') is-invalid @enderror">
@error('memo')
@ -51,7 +52,7 @@
<div class="form-group">
<label for="credits">Credits *</label>
<input value="{{old('credits')}}" placeholder="500" id="credits"
<input value="{{ $voucher->credits }}" placeholder="500" id="credits"
name="credits" type="number" step="any" min="0"
max="999999"
class="form-control @error('credits') is-invalid @enderror">
@ -66,7 +67,7 @@
<div class="form-group">
<label for="code">Code *</label>
<div class="input-group">
<input value="{{old('code')}}" placeholder="SUMMER" id="code" name="code"
<input value="{{ $voucher->code }}" placeholder="SUMMER" id="code" name="code"
type="text"
class="form-control @error('code') is-invalid @enderror"
required="required">
@ -86,7 +87,7 @@
<div class="form-group">
<label for="uses">Uses *</label>
<div class="input-group">
<input value="{{old('uses') ?? 1}}" id="uses" min="1" max="2147483647"
<input value="{{ $voucher->uses }}" id="uses" min="1" max="2147483647"
name="uses" type="number"
class="form-control @error('uses') is-invalid @enderror"
required="required">
@ -105,7 +106,7 @@
<div class="form-group">
<label for="expires_at">Expires at</label>
<input value="{{old('expires_at')}}" id="expires_at" name="expires_at"
<input value="{{ $voucher->expires_at }}" id="expires_at" name="expires_at"
type="datetime-local"
class="form-control @error('expires_at') is-invalid @enderror">
@error('expires_at')

View file

@ -6,13 +6,13 @@
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1>Products</h1>
<h1>Vouchers</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{route('home')}}">Dashboard</a></li>
<li class="breadcrumb-item"><a class="text-muted"
href="{{route('admin.products.index')}}">Products</a></li>
href="{{route('admin.vouchers.index')}}">Vouchers</a></li>
</ol>
</div>
</div>
@ -28,8 +28,8 @@
<div class="card-header">
<div class="d-flex justify-content-between">
<h5 class="card-title"><i class="fas fa-sliders-h mr-2"></i>Products</h5>
<a href="{{route('admin.products.create')}}" class="btn btn-sm btn-primary"><i
<h5 class="card-title"><i class="fas fa-money-check-alt mr-2"></i>Vouchers</h5>
<a href="{{route('admin.vouchers.create')}}" class="btn btn-sm btn-primary"><i
class="fas fa-plus mr-1"></i>Create new</a>
</div>
</div>
@ -39,19 +39,12 @@
<table id="datatable" class="table table-striped">
<thead>
<tr>
<th>Active</th>
<th>Name</th>
<th>Price</th>
<th>Memory</th>
<th>Cpu</th>
<th>Swap</th>
<th>Disk</th>
<th>IO</th>
<th>Databases</th>
<th>Backups</th>
<th>Allocations</th>
<th>Servers</th>
<th>Created at</th>
<th>Status</th>
<th>Code</th>
<th>Memo</th>
<th>Credits</th>
<th>Used / Uses</th>
<th>Expires</th>
<th></th>
</tr>
</thead>
@ -79,21 +72,14 @@
processing: true,
serverSide: true,
stateSave: true,
ajax: "{{route('admin.products.datatable')}}",
ajax: "{{route('admin.vouchers.datatable')}}",
columns: [
{data: 'disabled'},
{data: 'name'},
{data: 'price'},
{data: 'memory'},
{data: 'cpu'},
{data: 'swap'},
{data: 'disk'},
{data: 'io'},
{data: 'databases'},
{data: 'backups'},
{data: 'allocations'},
{data: 'servers', sortable: false},
{data: 'created_at'},
{data: 'status'},
{data: 'code'},
{data: 'memo'},
{data: 'credits'},
{data: 'uses'},
{data: 'expires_at'},
{data: 'actions', sortable: false},
],
fnDrawCallback: function( oSettings ) {

View file

@ -96,6 +96,10 @@
Log back in
</a>
@endif
<a class="dropdown-item" data-toggle="modal" data-target="#redeemVoucherModal" href="javascript:void(0)">
<i class="fas fa-money-check-alt fa-sm fa-fw mr-2 text-gray-400"></i>
Redeem code
</a>
<div class="dropdown-divider"></div>
<form method="post" action="{{route('logout')}}">
@csrf
@ -253,6 +257,14 @@
</a>
</li>
<li class="nav-item">
<a href="{{route('admin.vouchers.index')}}"
class="nav-link @if(Request::routeIs('admin.vouchers.*')) active @endif">
<i class="nav-icon fas fa-money-check-alt"></i>
<p>Vouchers</p>
</a>
</li>
<li class="nav-item">
<a href="{{route('admin.usefullinks.index')}}"
class="nav-link @if(Request::routeIs('admin.usefullinks.*')) active @endif">
@ -261,7 +273,6 @@
</a>
</li>
@endif
</ul>
@ -289,6 +300,8 @@
@endif
@yield('content')
@include('models.redeem_voucher_modal')
</div>
<!-- /.content-wrapper -->
<footer class="main-footer">
@ -319,6 +332,12 @@
<script>
$(document).ready(function () {
$('[data-toggle="popover"]').popover();
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
});
</script>
<script>

View file

@ -0,0 +1,51 @@
<!-- Button to Open the Modal -->
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#redeemVoucherModal">
Open modal
</button>
<!-- The Modal -->
<div class="modal fade" id="redeemVoucherModal">
<div class="modal-dialog">
<div class="modal-content">
<!-- Modal Header -->
<div class="modal-header">
<h4 class="modal-title">Redeem voucher code</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<!-- Modal body -->
<div class="modal-body">
<form>
<div class="form-group">
<label for="code">Code</label>
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">
<i class="fas fa-money-check-alt"></i>
</div>
</div>
<input id="code" name="code" placeholder="SUMMER" type="text" class="form-control">
</div>
</div>
</form>
</div>
<!-- Modal footer -->
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-dismiss="modal">Close</button>
<button name="submit" type="button" class="btn btn-primary">Redeem</button>
</div>
</div>
</div>
</div>
<script>
function validateCode(){
$.ajax({
})
}
</script>

View file

@ -12,6 +12,7 @@ use App\Http\Controllers\Admin\ServerController as AdminServerController;
use App\Http\Controllers\Admin\SettingsController;
use App\Http\Controllers\Admin\UsefulLinkController;
use App\Http\Controllers\Admin\UserController;
use App\Http\Controllers\Admin\VoucherController;
use App\Http\Controllers\Auth\SocialiteController;
use App\Http\Controllers\HomeController;
use App\Http\Controllers\NotificationController;
@ -110,6 +111,9 @@ Route::middleware('auth')->group(function () {
Route::get('usefullinks/datatable', [UsefulLinkController::class, 'datatable'])->name('usefullinks.datatable');
Route::resource('usefullinks', UsefulLinkController::class);
Route::get('vouchers/datatable', [VoucherController::class, 'datatable'])->name('vouchers.datatable');
Route::resource('vouchers', VoucherController::class);
Route::get('api/datatable', [ApplicationApiController::class, 'datatable'])->name('api.datatable');
Route::resource('api', ApplicationApiController::class)->parameters([
'api' => 'applicationApi',