crud and import voucher done

dev
Aji Kamaludin 1 year ago
parent b709a716e4
commit 78144c25bc
No known key found for this signature in database
GPG Key ID: 19058F67F0083AD3

@ -7,13 +7,13 @@
- [x] CRUD Rekening / Account
- [x] CRUD Customer
- [x] CRUD Lokasi
- [ ] CRUD Voucher
- [ ] Import Voucher
- [x] CRUD Voucher
- [x] Import Voucher
- [x] Setting Web (enable affilate, amount bonus affilate)
- [ ] Setting Level Customer (view levels, edit name of level,minimal saldo, max amount saldo, max hutang)
- [ ] Deposit Menu (view daftar histori deposit)
- [ ] Manual Approve Deposit
- [x] Setting Web (enable affilate, amount bonus affilate)
- [ ] Setting Bonus Coin (memasukan amount bonus coin yang didapat dengan level dan harga voucher) - coin rewards
- [ ] Setting Level Customer (view levels, edit name of level,minimal saldo, max amount saldo, max hutang)
- [ ] View Customer Coin History
- [ ] List Customer Verification
- [ ] Manual Approve Verification

@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Location;
use Illuminate\Http\Request;
class LocationController extends Controller
{
public function index(Request $request)
{
$query = Location::orderBy('updated_at', 'desc');
if ($request->q != '') {
$query->where('name', 'like', "%$request->q%");
}
return $query->get();
}
}

@ -0,0 +1,156 @@
<?php
namespace App\Http\Controllers;
use App\Models\Voucher;
use App\Services\GeneralService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class VoucherController extends Controller
{
public function index(Request $request)
{
$query = Voucher::with(['location'])->orderBy('updated_at', 'desc');
if ($request->q != '') {
$query->where('username', 'like', "%$request->q%")
->orWhere('comment', 'like', "%$request->q%")
->orWhere('profile', 'like', "%$request->q%");
}
return inertia('Voucher/Index', [
'query' => $query->paginate()
]);
}
public function create()
{
return inertia('Voucher/Form');
}
public function store(Request $request)
{
$request->validate([
'name' => 'nullable|string',
'description' => 'nullable|string',
'location_id' => 'required|exists:locations,id',
'username' => 'required|string',
'password' => 'required|string',
'discount' => 'required|numeric',
'display_price' => 'required|numeric',
'quota' => 'required|string',
'profile' => 'required|string',
'comment' => 'required|string',
'expired' => 'required|numeric',
'expired_unit' => 'required|string',
]);
Voucher::create([
'name' => $request->name,
'description' => $request->description,
'location_id' => $request->location_id,
'username' => $request->username,
'password' => $request->password,
'discount' => $request->discount,
'display_price' => $request->display_price,
'quota' => $request->quota,
'profile' => $request->profile,
'comment' => $request->comment,
'expired' => $request->expired,
'expired_unit' => $request->expired_unit,
]);
return redirect()->route('voucher.index')
->with('message', ['type' => 'success', 'message' => 'Item has beed saved']);
}
public function edit(Voucher $voucher)
{
return inertia('Voucher/Form', [
'voucher' => $voucher,
]);
}
public function update(Request $request, Voucher $voucher)
{
$request->validate([
'name' => 'nullable|string',
'description' => 'nullable|string',
'location_id' => 'required|exists:locations,id',
'username' => 'required|string',
'password' => 'required|string',
'discount' => 'required|numeric',
'display_price' => 'required|numeric',
'quota' => 'required|string',
'profile' => 'required|string',
'comment' => 'required|string',
'expired' => 'required|numeric',
'expired_unit' => 'required|string',
]);
$voucher->update([
'name' => $request->name,
'description' => $request->description,
'location_id' => $request->location_id,
'username' => $request->username,
'password' => $request->password,
'discount' => $request->discount,
'display_price' => $request->display_price,
'quota' => $request->quota,
'profile' => $request->profile,
'comment' => $request->comment,
'expired' => $request->expired,
]);
return redirect()->route('voucher.index')
->with('message', ['type' => 'success', 'message' => 'Item has beed updated']);
}
public function destroy(Voucher $voucher)
{
$voucher->delete();
return redirect()->route('voucher.index')
->with('message', ['type' => 'success', 'message' => 'Item has beed deleted']);
}
public function form_import()
{
return inertia('Voucher/Import');
}
public function import(Request $request)
{
$request->validate([
'script' => 'required|string',
'location_id' => 'required|exists:locations,id',
'discount' => 'required|numeric',
'display_price' => 'required|numeric',
'expired' => 'required|numeric',
'expired_unit' => 'required|string',
]);
$vouchers = GeneralService::script_parser($request->script);
DB::beginTransaction();
foreach ($vouchers as $voucher) {
Voucher::create([
'location_id' => $request->location_id,
'username' => $voucher['username'],
'password' => $voucher['password'],
'discount' => $request->discount,
'display_price' => $request->display_price,
'quota' => $voucher['quota'],
'profile' => $voucher['profile'],
'comment' => $voucher['comment'],
'expired' => $request->expired,
'expired_unit' => $request->expired_unit,
]);
}
DB::commit();
return redirect()->route('voucher.index')
->with('message', ['type' => 'success', 'message' => 'Items has beed saved']);
}
}

@ -30,13 +30,15 @@ class HandleInertiaCustomerRequests extends Middleware
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'app_name' => env('APP_NAME', 'App Name'),
'auth' => [
'user' => auth('customer')->user()?->load(['level']),
],
'flash' => [
'message' => fn () => $request->session()->get('message') ?? ['type' => null, 'message' => null],
],
'app_name' => env('APP_NAME', 'App Name'),
'notification_count' => 0,
'carts' => []
]);
}
}

@ -47,7 +47,7 @@ class Customer extends Authenticatable
protected static function booted(): void
{
static::creating(function (Customer $customer) {
if ($customer->customer_level_id == null) {
if ($customer->customer_level_id == '') {
$basic = CustomerLevel::where('key', CustomerLevel::BASIC)->first();
$customer->customer_level_id = $basic->id;

@ -19,5 +19,10 @@ class DepositHistory extends Model
'related_id',
'is_valid',
'image_prove',
'payment_token',
'payment_status',
'payment_response',
'payment_channel',
'payment_type',
];
}

@ -2,6 +2,9 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Str;
class Voucher extends Model
{
protected $fillable = [
@ -10,15 +13,55 @@ class Voucher extends Model
'location_id',
'username',
'password',
'price',
'price', // harga jual
'discount',
'display_price',
'display_price', //yang di input user
'quota',
'profile',
'comment',
'expired',
'expired_unit',
'is_sold', //menandakan sudah terjual atau belum
// batch pada saat import , jadi ketika user ingin beli akan tetapi sudah sold ,
// maka akan dicarikan voucher lain dari batch yang sama
'batch_id',
];
protected $appends = ['display_quota'];
protected static function booted(): void
{
static::creating(function (Voucher $voucher) {
if ($voucher->batch_id == '') {
$voucher->batch_id = Str::ulid();
}
if ($voucher->price == '') {
$price = $voucher->display_price;
if ($voucher->discount > 0) {
$price = $voucher->display_price - round($voucher->display_price * ($voucher->discount / 100), 2);
}
$voucher->price = $price;
}
});
static::updating(function (Voucher $voucher) {
$price = $voucher->display_price;
if ($voucher->discount > 0) {
$price = $voucher->display_price - round($voucher->display_price * ($voucher->discount / 100), 2);
}
$voucher->price = $price;
});
}
public function displayQuota(): Attribute
{
return Attribute::make(get: function () {
return round($this->quota / (1024 * 1024 * 1024), 2) . ' GB';
});
}
public function location()
{
return $this->belongsTo(Location::class);

@ -0,0 +1,53 @@
<?php
namespace App\Services;
class GeneralService
{
public static function script_parser($script)
{
$data = [];
$lines = explode("\n", $script);
foreach ($lines as $line) {
$item = self::line_parser($line);
if ($item != null) {
$data[] = $item;
}
}
return $data;
}
public static function line_parser($line)
{
$item = null;
$commands = explode(' ', $line);
foreach ($commands as $command) {
if (str_contains($command, 'name')) {
$d = explode('=', $command);
$item['username'] = str_replace('"', '', $d[1]);
}
if (str_contains($command, 'password')) {
$d = explode('=', $command);
$item['password'] = str_replace('"', '', $d[1]);
}
if (str_contains($command, 'profile')) {
$d = explode('=', $command);
$item['profile'] = str_replace('"', '', $d[1]);
}
if (str_contains($command, 'comment')) {
$d = explode('=', $command);
$item['comment'] = str_replace('"', '', $d[1]);
}
if (str_contains($command, 'limit-bytes-total')) {
$d = explode('=', $command);
$item['quota'] = (int) str_replace('"', '', $d[1]);
}
}
info('item', [$item]);
return $item;
}
}

@ -0,0 +1,60 @@
<?php
namespace App\Services;
use Midtrans\Config;
use Midtrans\Snap;
class MidtransService
{
protected $order;
public function __construct($order, $serverKey)
{
Config::$serverKey = $serverKey;
Config::$isProduction = app()->isProduction();
Config::$isSanitized = true;
Config::$is3ds = true;
$this->order = $order;
}
public function getSnapToken()
{
$items = $this->order->items->map(function ($item) {
return [
'id' => $item->id,
'price' => $item->amount,
'quantity' => $item->quantity,
'name' => $item->item->order_detail,
];
});
if ($this->order->total_discount > 0) {
$items->add([
'id' => 'Discount',
'price' => -$this->order->total_discount,
'quantity' => 1,
'name' => 'DISCOUNT',
]);
}
$params = [
'transaction_details' => [
'order_id' => $this->order->order_code,
'gross_amount' => $this->order->total_amount,
],
'item_details' => $items->toArray(),
'customer_details' => [
'name' => $this->order->customer->name,
'email' => $this->order->customer->email,
'phone' => $this->order->customer->phone,
'address' => $this->order->customer->address,
],
];
$snapToken = Snap::getSnapToken($params);
return $snapToken;
}
}

@ -13,6 +13,7 @@
"laravel/sanctum": "^3.2.5",
"laravel/socialite": "^5.6",
"laravel/tinker": "^2.8.1",
"midtrans/midtrans-php": "^2.5",
"react/async": "^4",
"socialiteproviders/google": "^4.1",
"tightenco/ziggy": "^1.6.0"

57
composer.lock generated

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6596d1096f48206df6c62070e7c12354",
"content-hash": "79583c93a70926c9a3247063a03be945",
"packages": [
{
"name": "brick/math",
@ -2024,6 +2024,61 @@
},
"time": "2022-04-15T14:02:14+00:00"
},
{
"name": "midtrans/midtrans-php",
"version": "2.5.2",
"source": {
"type": "git",
"url": "https://github.com/Midtrans/midtrans-php.git",
"reference": "a1ad0c824449ca8c68c4cf11b3417ad518311d2b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Midtrans/midtrans-php/zipball/a1ad0c824449ca8c68c4cf11b3417ad518311d2b",
"reference": "a1ad0c824449ca8c68c4cf11b3417ad518311d2b",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"php": ">=5.4"
},
"require-dev": {
"phpunit/phpunit": "5.7.*",
"psy/psysh": "0.4.*"
},
"type": "library",
"autoload": {
"psr-4": {
"Midtrans\\": "Midtrans/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Andri Setiawan",
"email": "andri.setiawan@veritrans.co.id"
},
{
"name": "Alvin Litani",
"email": "alvin.litani@veritrans.co.id"
},
{
"name": "Ismail Faruqi",
"email": "ismail.faruqi@veritrans.co.id"
}
],
"description": "PHP Wrapper for Midtrans Payment API.",
"homepage": "https://midtrans.com",
"support": {
"issues": "https://github.com/Midtrans/midtrans-php/issues",
"source": "https://github.com/Midtrans/midtrans-php/tree/2.5.2"
},
"time": "2021-08-23T08:52:05+00:00"
},
{
"name": "monolog/monolog",
"version": "3.3.1",

@ -26,6 +26,10 @@ return new class extends Migration
$table->string('profile')->nullable();
$table->text('comment')->nullable();
$table->string('expired')->nullable();
$table->string('expired_unit')->nullable();
$table->smallInteger('is_sold')->default(0);
$table->ulid('batch_id')->nullable();
$table->text('additional_json')->nullable();
$table->timestamps();
$table->softDeletes();

@ -22,6 +22,11 @@ return new class extends Migration
$table->string('related_id')->nullable();
$table->smallInteger('is_valid')->default(0);
$table->string('image_prove')->nullable();
$table->string('payment_token')->nullable();
$table->string('payment_status')->nullable();
$table->string('payment_response')->nullable();
$table->string('payment_channel')->nullable();
$table->string('payment_type')->nullable();
$table->timestamps();
$table->softDeletes();

@ -0,0 +1,64 @@
add name="8ga6xmzzkn" password="8ga6xmzzkn" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="8gd5874ncu" password="8gd5874ncu" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="8g5t5tax37" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="8g5t5tax38" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="8g5t5tax39" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="8g5t5tax10" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="8g5t5tax11" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="8g5t5tax12" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="8g5t5tax13" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="8g5t5tax14" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="8ga6xmzzkn1" password="8ga6xmzzkn" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="8gd5874ncu2" password="8gd5874ncu" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="8g5t5tax373" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="8g5t5tax384" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="8g5t5tax395" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="8g5t5tax106" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="8g5t5tax117" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="8g5t5tax128" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="8g5t5tax139" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="8g5t5tax1410" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="YvTdU80eaq" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="IckwYPBq7m" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="25HH4VlPoN" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="Sc5LesGx1S" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="XiMhZXBeyb" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="kaEXtorL8V" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="EfKbqsSuSs" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="3QZJaf5bcJ" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="vPXQF5ZZb3" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="yYKLQFjJll" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="sw7Oi5WznU" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="DYcTEEyZCz" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="m5XTZNFYmq" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="E4k5tXWBsH" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="8ky5sMmDUB" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="7Cbm4U52AN" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="Vdkw5NU312" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="NW6GXomQyF" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="SsilG1ZkkM" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="8D8oPBRfhm" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="0GL2GwD9fl" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="Z1f3ZpplaD" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="E1MVCSIvjR" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="1S2xkQ9ofv" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="VdX0x5GjS9" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="xTIRAtrkM4" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="RYvmb03wOD" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="Ji6AEYaJmF" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="yLHQlK79cZ" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="BBvTG8TWv0" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="sMQ4jlCVJB" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="iaQNtPMcQH" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="HsUIUagLX4" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="xUrvtZpRL3" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="Yyh0gjfmQR" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="FHMFQd9MpW" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="pliCYDGkSd" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="nkNvbK2W6e" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="jIGd68CJjM" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
add name="Jm3WDPCNSw" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"
foreach(range(1,40) as $r) {
echo 'add name="'. Str::random(10) .'" password="8g5t5tax37" profile="NEW-8GB-Kuota-7726-MB" comment="vc-203-06.17.22-a1" limit-bytes-total="8101298176"'."\n";
}

@ -1,5 +1,6 @@
import React from 'react'
import Input from './Input'
import { isEmpty } from 'lodash'
export default function FormInputWith({
type,
@ -16,7 +17,10 @@ export default function FormInputWith({
readOnly,
onKeyDownCapture,
leftItem,
rightItem,
formClassName,
max,
min,
}) {
return (
<div className={className}>
@ -27,9 +31,11 @@ export default function FormInputWith({
{label}
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
{leftItem}
</div>
{isEmpty(leftItem) === false && (
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
{leftItem}
</div>
)}
<input
type={type}
className={`mb-2 bg-gray-50 border text-gray-900 text-sm rounded-lg block w-full p-2.5 dark:bg-gray-700 dark:placeholder-gray-400 dark:text-white ${
@ -46,7 +52,14 @@ export default function FormInputWith({
disabled={disabled}
readOnly={readOnly}
onKeyDownCapture={onKeyDownCapture}
max={max}
min={min}
/>
{isEmpty(rightItem) === false && (
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
{rightItem}
</div>
)}
</div>
{error && (
<p className="mb-2 text-sm text-red-600 dark:text-red-500">

@ -11,6 +11,7 @@ import {
HiMap,
HiOutlineGlobeAlt,
HiQuestionMarkCircle,
HiTicket,
HiUserCircle,
} from 'react-icons/hi2'
@ -55,6 +56,14 @@ export default [
active: 'location.*',
permission: 'view-location',
},
{
name: 'Voucher',
show: true,
icon: HiTicket,
route: route('voucher.index'),
active: 'voucher.*',
permission: 'view-voucher',
},
{
name: 'Front Home',
show: true,

@ -0,0 +1,263 @@
import React, { useRef, useEffect, useState } from 'react'
import { useDebounce } from '@/hooks'
import { usePage } from '@inertiajs/react'
import axios from 'axios'
import { HiChevronDown, HiChevronUp, HiX } from 'react-icons/hi'
import { Spinner } from 'flowbite-react'
export default function SelectionInput(props) {
const ref = useRef()
const {
props: { auth },
} = usePage()
const {
label = '',
itemSelected = null,
onItemSelected = () => {},
disabled = false,
placeholder = '',
error = '',
all = 0,
} = props
const [showItems, setShowItem] = useState([])
const [isSelected, setIsSelected] = useState(true)
const [selected, setSelected] = useState(null)
const [query, setQuery] = useState('')
const q = useDebounce(query, 300)
const [isOpen, setIsOpen] = useState(false)
const [loading, setLoading] = useState(false)
const toggle = () => {
setQuery('')
setIsOpen(!isOpen)
}
const onInputMouseDown = () => {
setIsSelected(false)
setQuery('')
setIsOpen(!isOpen)
}
const handleSelectItem = (item) => {
setIsSelected(true)
onItemSelected(item.id)
setSelected(item.name)
setIsOpen(false)
}
const removeItem = () => {
setIsSelected(false)
setSelected('')
onItemSelected(null)
}
const filterItems = (value) => {
setIsSelected(false)
setQuery(value)
}
useEffect(() => {
if (isOpen === true) {
const checkIfClickedOutside = (e) => {
if (isOpen && ref.current && !ref.current.contains(e.target)) {
setIsOpen(false)
if (selected !== null) {
setIsSelected(true)
}
}
}
document.addEventListener('mousedown', checkIfClickedOutside)
return () => {
document.removeEventListener('mousedown', checkIfClickedOutside)
}
}
}, [isOpen])
const fetch = (q = '') => {
setLoading(true)
axios
.get(route('api.location.index', { q: q, all: all }), {
headers: {
'Content-Type': 'application/json',
// Authorization: 'Bearer ' + auth.user.jwt_token,
},
})
.then((response) => {
setShowItem(response.data)
})
.catch((err) => {
alert(err)
})
.finally(() => setLoading(false))
}
// every select item open
useEffect(() => {
if (isOpen) {
fetch(q)
}
}, [q, isOpen])
// once page load
useEffect(() => {
fetch()
}, [])
useEffect(() => {
if (disabled) {
setSelected('')
}
}, [disabled])
useEffect(() => {
if (itemSelected !== null) {
const item = showItems.find((item) => item.id === itemSelected)
if (item) {
setSelected(item.name)
setIsSelected(true)
}
return
}
setIsSelected(false)
}, [itemSelected, loading])
useEffect(() => {
if (isSelected && selected === '') {
setSelected('')
setIsSelected(false)
}
}, [isSelected])
return (
<div className="flex flex-col items-center" ref={ref}>
<div className="w-full flex flex-col items-center">
<div className="w-full">
<div className="flex flex-col relative">
{label !== '' && (
<label
htmlFor="first_name"
className="mb-2 block text-sm font-medium text-gray-900 dark:text-white"
>
{label}
</label>
)}
<div className="w-full">
<div
className={`p-1.5 bg-gray-50 dark:bg-gray-700 flex border rounded-lg
${
error
? 'border-red-500'
: 'border-gray-300 dark:border-gray-600'
}
${disabled ? 'bg-gray-50' : ''}`}
>
<input
className="block w-full text-sm bg-gray-50 text-gray-900 dark:border-gray-700 border cursor-pointer dark:text-gray-300 outline-none border-transparent dark:bg-gray-700 dark:placeholder-gray-400 px-1"
onMouseDown={onInputMouseDown}
placeholder={placeholder}
value={`${
isSelected
? selected === null
? ''
: selected
: query
}`}
onChange={(e) =>
filterItems(e.target.value)
}
disabled={disabled}
/>
{isSelected && (
<div
onClick={
disabled ? () => {} : removeItem
}
>
<button className="cursor-pointer w-6 h-6 text-red-300 outline-none focus:outline-none">
<HiX />
</button>
</div>
)}
<div onClick={disabled ? () => {} : toggle}>
<button className="cursor-pointer w-6 h-6 text-gray-300 outline-none focus:outline-none">
{isOpen ? (
<HiChevronUp />
) : (
<HiChevronDown />
)}
</button>
</div>
</div>
{error && (
<p className="mb-2 text-sm text-red-600 dark:text-red-500">
{error}
</p>
)}
</div>
{isOpen && (
<div
className="absolute mt-1 shadow-lg bg-gray-50 dark:bg-gray-700 dark:text-gray-200 top-100 z-40 w-full lef-0 rounded overflow-y-auto"
style={{ maxHeight: '300px', top: '100%' }}
>
<div className="flex flex-col w-full">
{loading ? (
<div>
<div className="flex w-full items-center p-2 pl-2 border-transparent border-l-2 relative hover:border-neutral-content">
<div className="w-full items-center justify-center flex">
<div className="mx-2 my-5">
<Spinner className="mr-2" />
<span>Loading...</span>
</div>
</div>
</div>
</div>
) : (
<>
{showItems.map((item, index) => (
<div
key={index}
onClick={() =>
handleSelectItem(item)
}
>
<div className="flex w-full items-center p-2 pl-2 border-transparent border-l-2 relative hover:border-neutral-content hover:bg-gray-400 bg-opacity-10 dark:hover:bg-gray-200 dark:hover:bg-opacity-10 dark:hover:text-gray-100">
<div className="w-full items-center flex">
<div className="mx-2">
<span>
{item.name}
</span>
</div>
</div>
</div>
</div>
))}
{showItems.length <= 0 && (
<div>
<div className="flex w-full items-center p-2 pl-2 border-transparent border-l-2 relative hover:border-neutral-content">
<div className="w-full items-center justify-center flex">
<div className="mx-2 my-5">
<span>
No Items
Found
</span>
</div>
</div>
</div>
</div>
)}
</>
)}
</div>
</div>
)}
</div>
</div>
</div>
</div>
)
}

@ -0,0 +1,182 @@
import React, { useEffect } from 'react'
import { isEmpty } from 'lodash'
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'
import FormInput from '@/Components/FormInput'
import Button from '@/Components/Button'
import { Head, useForm } from '@inertiajs/react'
import FormInputWith from '@/Components/FormInputWith'
import LocationSelectionInput from '../Location/SelectionInput'
export default function Form(props) {
const { voucher } = props
const { data, setData, post, processing, errors } = useForm({
username: '',
password: '',
discount: 0,
display_price: 0,
quota: '',
profile: '',
comment: '',
expired: '',
expired_unit: 'Hari',
location_id: null,
})
const handleOnChange = (event) => {
setData(
event.target.name,
event.target.type === 'checkbox'
? event.target.checked
? 1
: 0
: event.target.value
)
}
const handleSubmit = () => {
if (isEmpty(voucher) === false) {
post(route('voucher.update', voucher))
return
}
post(route('voucher.store'))
}
useEffect(() => {
if (isEmpty(voucher) === false) {
setData({
username: voucher.username,
password: voucher.password,
discount: voucher.discount,
display_price: voucher.display_price,
quota: voucher.quota,
profile: voucher.profile,
comment: voucher.comment,
expired: voucher.expired,
expired_unit: voucher.expired_unit,
location_id: voucher.location_id,
})
}
}, [voucher])
return (
<AuthenticatedLayout
auth={props.auth}
errors={props.errors}
flash={props.flash}
page={'Voucher'}
action={'Form'}
>
<Head title="Voucher" />
<div>
<div className="mx-auto sm:px-6 lg:px-8">
<div className="overflow-hidden p-4 shadow-sm sm:rounded-lg bg-white dark:bg-gray-800 flex flex-col ">
<div className="text-xl font-bold mb-4">Voucher</div>
<LocationSelectionInput
label="Lokasi"
itemSelected={data.location_id}
onItemSelected={(id) => setData('location_id', id)}
error={errors.location_id}
/>
<div className="mt-2" />
<FormInput
name="username"
value={data.username}
onChange={handleOnChange}
label="Username"
error={errors.username}
/>
<FormInput
name="password"
value={data.password}
onChange={handleOnChange}
label="Password"
error={errors.password}
/>
<FormInput
type="number"
name="display_price"
value={data.display_price}
onChange={handleOnChange}
label="Harga"
error={errors.display_price}
/>
<FormInputWith
type="number"
rightItem={<div className="text-sm">%</div>}
name="discount"
value={data.discount}
onChange={handleOnChange}
error={errors.discount}
formClassName={'pr-10'}
label="Discount"
max={100}
min={0}
/>
<FormInput
type="number"
name="quota"
value={data.quota}
onChange={handleOnChange}
label="Kuota (bytes)"
error={errors.quota}
/>
<FormInput
name="profile"
value={data.profile}
onChange={handleOnChange}
label="Profile"
error={errors.profile}
/>
<FormInput
name="comment"
value={data.comment}
onChange={handleOnChange}
label="Comment"
error={errors.comment}
/>
<div>
<label className="block text-sm font-medium text-gray-900 dark:text-white">
Masa Aktif
</label>
<div className="w-full flex flex-row space-x-2 items-center">
<FormInput
type="number"
name="expired"
value={data.expired}
onChange={handleOnChange}
// label="Masa Aktif"
error={errors.expired}
className="flex-1"
/>
<div>
<select
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
onChange={handleOnChange}
value={data.expired_unit}
name="expired_unit"
>
<option value="Jam">Jam</option>
<option value="Hari">Hari</option>
<option value="Minggu">Minggu</option>
<option value="Bulan">Bulan</option>
<option value="Tahun">Tahun</option>
</select>
</div>
</div>
</div>
<div className="mt-8">
<Button
onClick={handleSubmit}
processing={processing}
>
Simpan
</Button>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
)
}

@ -0,0 +1,128 @@
import React, { useEffect } from 'react'
import { isEmpty } from 'lodash'
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'
import FormInput from '@/Components/FormInput'
import Button from '@/Components/Button'
import { Head, useForm } from '@inertiajs/react'
import FormInputWith from '@/Components/FormInputWith'
import LocationSelectionInput from '../Location/SelectionInput'
import TextArea from '@/Components/TextArea'
export default function Import(props) {
const { data, setData, post, processing, errors } = useForm({
script: '',
discount: 0,
display_price: 0,
expired: '',
expired_unit: 'Hari',
location_id: null,
})
const handleOnChange = (event) => {
setData(
event.target.name,
event.target.type === 'checkbox'
? event.target.checked
? 1
: 0
: event.target.value
)
}
const handleSubmit = () => {
post(route('voucher.import'))
}
return (
<AuthenticatedLayout
auth={props.auth}
errors={props.errors}
flash={props.flash}
page={'Voucher'}
action={'Import'}
>
<Head title="Voucher" />
<div>
<div className="mx-auto sm:px-6 lg:px-8">
<div className="overflow-hidden p-4 shadow-sm sm:rounded-lg bg-white dark:bg-gray-800 flex flex-col ">
<div className="text-xl font-bold mb-4">Voucher</div>
<LocationSelectionInput
label="Lokasi"
itemSelected={data.location_id}
onItemSelected={(id) => setData('location_id', id)}
error={errors.location_id}
/>
<div className="mt-2" />
<FormInput
type="number"
name="display_price"
value={data.display_price}
onChange={handleOnChange}
label="Harga"
error={errors.display_price}
/>
<FormInputWith
type="number"
rightItem={<div className="text-sm">%</div>}
name="discount"
value={data.discount}
onChange={handleOnChange}
error={errors.discount}
formClassName={'pr-10'}
label="Discount"
max={100}
min={0}
/>
<div>
<label className="block text-sm font-medium text-gray-900 dark:text-white">
Masa Aktif
</label>
<div className="w-full flex flex-row space-x-2 items-center">
<FormInput
type="number"
name="expired"
value={data.expired}
onChange={handleOnChange}
// label="Masa Aktif"
error={errors.expired}
className="flex-1"
/>
<div>
<select
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
onChange={handleOnChange}
value={data.expired_unit}
name="expired_unit"
>
<option value="Jam">Jam</option>
<option value="Hari">Hari</option>
<option value="Minggu">Minggu</option>
<option value="Bulan">Bulan</option>
<option value="Tahun">Tahun</option>
</select>
</div>
</div>
</div>
<TextArea
name="script"
value={data.script}
onChange={handleOnChange}
label="Script"
error={errors.script}
rows={16}
/>
<div className="mt-8">
<Button
onClick={handleSubmit}
processing={processing}
>
Simpan
</Button>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
)
}

@ -0,0 +1,234 @@
import React, { useEffect, useState } from 'react'
import { Link, router } from '@inertiajs/react'
import { usePrevious } from 'react-use'
import { Head } from '@inertiajs/react'
import { Button, Dropdown } from 'flowbite-react'
import { HiPencil, HiTrash } from 'react-icons/hi'
import { useModalState } from '@/hooks'
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'
import Pagination from '@/Components/Pagination'
import ModalConfirm from '@/Components/ModalConfirm'
import SearchInput from '@/Components/SearchInput'
import { hasPermission } from '@/utils'
export default function Index(props) {
const {
query: { links, data },
auth,
} = props
const [search, setSearch] = useState('')
const preValue = usePrevious(search)
const confirmModal = useModalState()
const handleDeleteClick = (voucher) => {
confirmModal.setData(voucher)
confirmModal.toggle()
}
const onDelete = () => {
if (confirmModal.data !== null) {
router.delete(route('voucher.destroy', confirmModal.data.id))
}
}
const params = { q: search }
useEffect(() => {
if (preValue) {
router.get(
route(route().current()),
{ q: search },
{
replace: true,
preserveState: true,
}
)
}
}, [search])
const canCreate = hasPermission(auth, 'create-voucher')
const canUpdate = hasPermission(auth, 'update-voucher')
const canDelete = hasPermission(auth, 'delete-voucher')
return (
<AuthenticatedLayout
auth={props.auth}
errors={props.errors}
flash={props.flash}
page={'Voucher'}
action={''}
>
<Head title="Voucher" />
<div>
<div className="mx-auto sm:px-6 lg:px-8 ">
<div className="p-6 overflow-hidden shadow-sm sm:rounded-lg bg-gray-200 dark:bg-gray-800 space-y-4">
<div className="flex justify-between">
{canCreate && (
<div className="flex flex-row space-x-2">
<Link href={route('voucher.create')}>
<Button size="sm">Tambah</Button>
</Link>
<Link href={route('voucher.import')}>
<Button size="sm" outline>
Import
</Button>
</Link>
</div>
)}
<div className="flex items-center">
<SearchInput
onChange={(e) => setSearch(e.target.value)}
value={search}
/>
</div>
</div>
<div className="overflow-auto">
<div>
<table className="w-full text-sm text-left text-gray-500 dark:text-gray-400 mb-4">
<thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th
scope="col"
className="py-3 px-6"
>
No
</th>
<th
scope="col"
className="py-3 px-6"
>
Username
</th>
<th
scope="col"
className="py-3 px-6"
>
Password
</th>
<th
scope="col"
className="py-3 px-6"
>
Profile
</th>
<th
scope="col"
className="py-3 px-6"
>
Comment
</th>
<th
scope="col"
className="py-3 px-6"
>
Kuota
</th>
<th
scope="col"
className="py-3 px-6 w-1/8"
/>
</tr>
</thead>
<tbody>
{data.map((voucher, index) => (
<tr
className="bg-white border-b dark:bg-gray-800 dark:border-gray-700"
key={voucher.id}
>
<td
scope="row"
className="py-4 px-6 font-medium text-gray-900 whitespace-nowrap dark:text-white"
>
{index + 1}
</td>
<td
scope="row"
className="py-4 px-6"
>
{voucher.username}
</td>
<td
scope="row"
className="py-4 px-6"
>
{voucher.password}
</td>
<td
scope="row"
className="py-4 px-6"
>
{voucher.profile}
</td>
<td
scope="row"
className="py-4 px-6"
>
{voucher.comment}
</td>
<td
scope="row"
className="py-4 px-6"
>
{voucher.display_quota}
</td>
<td className="py-4 px-6 flex justify-end">
<Dropdown
label={'Opsi'}
floatingArrow={true}
arrowIcon={true}
dismissOnClick={true}
size={'sm'}
>
{canUpdate && (
<Dropdown.Item>
<Link
href={route(
'voucher.edit',
voucher
)}
className="flex space-x-1 items-center"
>
<HiPencil />
<div>
Ubah
</div>
</Link>
</Dropdown.Item>
)}
{canDelete && (
<Dropdown.Item
onClick={() =>
handleDeleteClick(
voucher
)
}
>
<div className="flex space-x-1 items-center">
<HiTrash />
<div>
Hapus
</div>
</div>
</Dropdown.Item>
)}
</Dropdown>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="w-full flex items-center justify-center">
<Pagination links={links} params={params} />
</div>
</div>
</div>
</div>
</div>
<ModalConfirm modalState={confirmModal} onConfirm={onDelete} />
</AuthenticatedLayout>
)
}

@ -11,6 +11,7 @@ use App\Http\Controllers\ProfileController;
use App\Http\Controllers\RoleController;
use App\Http\Controllers\SettingController;
use App\Http\Controllers\UserController;
use App\Http\Controllers\VoucherController;
use Illuminate\Support\Facades\Route;
Route::middleware(['http_secure_aware', 'inertia.admin'])
@ -84,6 +85,16 @@ Route::middleware(['http_secure_aware', 'inertia.admin'])
Route::post('/customers/{customer}', [CustomerController::class, 'update'])->name('customer.update');
Route::delete('/customers/{customer}', [CustomerController::class, 'destroy'])->name('customer.destroy');
// voucher
Route::get('/vouchers/import', [VoucherController::class, 'form_import'])->name('voucher.form_import');
Route::post('/vouchers/import', [VoucherController::class, 'import'])->name('voucher.import');
Route::get('/vouchers', [VoucherController::class, 'index'])->name('voucher.index');
Route::get('/vouchers/create', [VoucherController::class, 'create'])->name('voucher.create');
Route::post('/vouchers', [VoucherController::class, 'store'])->name('voucher.store');
Route::get('/vouchers/{voucher}', [VoucherController::class, 'edit'])->name('voucher.edit');
Route::post('/vouchers/{voucher}', [VoucherController::class, 'update'])->name('voucher.update');
Route::delete('/vouchers/{voucher}', [VoucherController::class, 'destroy'])->name('voucher.destroy');
// setting
Route::get('/settings', [SettingController::class, 'index'])->name('setting.index');
Route::post('/settings', [SettingController::class, 'update'])->name('setting.update');

@ -1,6 +1,7 @@
<?php
use App\Http\Controllers\Api\RoleController;
use App\Http\Controllers\Api\LocationController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
@ -19,5 +20,9 @@ Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
// general
Route::get('/roles', [RoleController::class, 'index'])->name('api.role.index');
Route::get('/locations', [LocationController::class, 'index'])->name('api.location.index');
// midtrans
Route::post('mindtrans/notification', fn () => 'Ok!')->name('api.midtrans.notification');

Loading…
Cancel
Save