From 78144c25bc5c66efcfa1d24eb958d889632aa22b Mon Sep 17 00:00:00 2001 From: Aji Kamaludin Date: Sat, 3 Jun 2023 00:30:19 +0700 Subject: [PATCH] crud and import voucher done --- TODO.md | 8 +- .../Controllers/Api/LocationController.php | 21 ++ app/Http/Controllers/VoucherController.php | 156 +++++++++++ .../HandleInertiaCustomerRequests.php | 4 +- app/Models/Customer.php | 2 +- app/Models/DepositHistory.php | 5 + app/Models/Voucher.php | 47 +++- app/Services/GeneralService.php | 53 ++++ app/Services/MidtransService.php | 60 ++++ composer.json | 1 + composer.lock | 57 +++- ...023_05_24_130522_create_vouchers_table.php | 4 + ..._130646_create_deposit_histories_table.php | 5 + public/example.md | 64 +++++ resources/js/Components/FormInputWith.jsx | 19 +- resources/js/Layouts/Partials/routes.js | 9 + .../js/Pages/Location/SelectionInput.jsx | 263 ++++++++++++++++++ resources/js/Pages/Voucher/Form.jsx | 182 ++++++++++++ resources/js/Pages/Voucher/Import.jsx | 128 +++++++++ resources/js/Pages/Voucher/Index.jsx | 234 ++++++++++++++++ routes/admin.php | 11 + routes/api.php | 5 + 22 files changed, 1326 insertions(+), 12 deletions(-) create mode 100644 app/Http/Controllers/Api/LocationController.php create mode 100644 app/Http/Controllers/VoucherController.php create mode 100644 app/Services/GeneralService.php create mode 100644 app/Services/MidtransService.php create mode 100644 public/example.md create mode 100644 resources/js/Pages/Location/SelectionInput.jsx create mode 100644 resources/js/Pages/Voucher/Form.jsx create mode 100644 resources/js/Pages/Voucher/Import.jsx create mode 100644 resources/js/Pages/Voucher/Index.jsx diff --git a/TODO.md b/TODO.md index 8a94fa0..790f9b9 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/app/Http/Controllers/Api/LocationController.php b/app/Http/Controllers/Api/LocationController.php new file mode 100644 index 0000000..2dbbc89 --- /dev/null +++ b/app/Http/Controllers/Api/LocationController.php @@ -0,0 +1,21 @@ +q != '') { + $query->where('name', 'like', "%$request->q%"); + } + + return $query->get(); + } +} diff --git a/app/Http/Controllers/VoucherController.php b/app/Http/Controllers/VoucherController.php new file mode 100644 index 0000000..3dd4885 --- /dev/null +++ b/app/Http/Controllers/VoucherController.php @@ -0,0 +1,156 @@ +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']); + } +} diff --git a/app/Http/Middleware/HandleInertiaCustomerRequests.php b/app/Http/Middleware/HandleInertiaCustomerRequests.php index 6f46927..88855fd 100644 --- a/app/Http/Middleware/HandleInertiaCustomerRequests.php +++ b/app/Http/Middleware/HandleInertiaCustomerRequests.php @@ -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' => [] ]); } } diff --git a/app/Models/Customer.php b/app/Models/Customer.php index 1d7e06a..c26fd94 100644 --- a/app/Models/Customer.php +++ b/app/Models/Customer.php @@ -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; diff --git a/app/Models/DepositHistory.php b/app/Models/DepositHistory.php index 425c263..f40e192 100644 --- a/app/Models/DepositHistory.php +++ b/app/Models/DepositHistory.php @@ -19,5 +19,10 @@ class DepositHistory extends Model 'related_id', 'is_valid', 'image_prove', + 'payment_token', + 'payment_status', + 'payment_response', + 'payment_channel', + 'payment_type', ]; } diff --git a/app/Models/Voucher.php b/app/Models/Voucher.php index 04ae0dd..767e9ed 100644 --- a/app/Models/Voucher.php +++ b/app/Models/Voucher.php @@ -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); diff --git a/app/Services/GeneralService.php b/app/Services/GeneralService.php new file mode 100644 index 0000000..2c1e63c --- /dev/null +++ b/app/Services/GeneralService.php @@ -0,0 +1,53 @@ +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; + } +} diff --git a/composer.json b/composer.json index 7295830..6f01abe 100644 --- a/composer.json +++ b/composer.json @@ -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" diff --git a/composer.lock b/composer.lock index 84f1c84..de9bb70 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/database/migrations/2023_05_24_130522_create_vouchers_table.php b/database/migrations/2023_05_24_130522_create_vouchers_table.php index e1a99fa..29b9593 100644 --- a/database/migrations/2023_05_24_130522_create_vouchers_table.php +++ b/database/migrations/2023_05_24_130522_create_vouchers_table.php @@ -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(); diff --git a/database/migrations/2023_05_24_130646_create_deposit_histories_table.php b/database/migrations/2023_05_24_130646_create_deposit_histories_table.php index ef676e5..96c8846 100644 --- a/database/migrations/2023_05_24_130646_create_deposit_histories_table.php +++ b/database/migrations/2023_05_24_130646_create_deposit_histories_table.php @@ -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(); diff --git a/public/example.md b/public/example.md new file mode 100644 index 0000000..40a370f --- /dev/null +++ b/public/example.md @@ -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"; +} diff --git a/resources/js/Components/FormInputWith.jsx b/resources/js/Components/FormInputWith.jsx index 8c27333..1d214f2 100644 --- a/resources/js/Components/FormInputWith.jsx +++ b/resources/js/Components/FormInputWith.jsx @@ -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 (
@@ -27,9 +31,11 @@ export default function FormInputWith({ {label}
-
- {leftItem} -
+ {isEmpty(leftItem) === false && ( +
+ {leftItem} +
+ )} + {isEmpty(rightItem) === false && ( +
+ {rightItem} +
+ )}
{error && (

diff --git a/resources/js/Layouts/Partials/routes.js b/resources/js/Layouts/Partials/routes.js index 03c3d75..502cd8c 100644 --- a/resources/js/Layouts/Partials/routes.js +++ b/resources/js/Layouts/Partials/routes.js @@ -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, diff --git a/resources/js/Pages/Location/SelectionInput.jsx b/resources/js/Pages/Location/SelectionInput.jsx new file mode 100644 index 0000000..940beb6 --- /dev/null +++ b/resources/js/Pages/Location/SelectionInput.jsx @@ -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 ( +

+
+
+
+ {label !== '' && ( + + )} +
+
+ + filterItems(e.target.value) + } + disabled={disabled} + /> + {isSelected && ( +
{} : removeItem + } + > + +
+ )} +
{} : toggle}> + +
+
+ {error && ( +

+ {error} +

+ )} +
+ {isOpen && ( +
+
+ {loading ? ( +
+
+
+
+ + Loading... +
+
+
+
+ ) : ( + <> + {showItems.map((item, index) => ( +
+ handleSelectItem(item) + } + > +
+
+
+ + {item.name} + +
+
+
+
+ ))} + {showItems.length <= 0 && ( +
+
+
+
+ + No Items + Found + +
+
+
+
+ )} + + )} +
+
+ )} +
+
+
+
+ ) +} diff --git a/resources/js/Pages/Voucher/Form.jsx b/resources/js/Pages/Voucher/Form.jsx new file mode 100644 index 0000000..974d1e0 --- /dev/null +++ b/resources/js/Pages/Voucher/Form.jsx @@ -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 ( + + + +
+
+
+
Voucher
+ setData('location_id', id)} + error={errors.location_id} + /> +
+ + + + %
} + name="discount" + value={data.discount} + onChange={handleOnChange} + error={errors.discount} + formClassName={'pr-10'} + label="Discount" + max={100} + min={0} + /> + + + +
+ +
+ +
+ +
+
+
+
+ +
+
+
+
+
+ ) +} diff --git a/resources/js/Pages/Voucher/Import.jsx b/resources/js/Pages/Voucher/Import.jsx new file mode 100644 index 0000000..b2deecd --- /dev/null +++ b/resources/js/Pages/Voucher/Import.jsx @@ -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 ( + + + +
+
+
+
Voucher
+ setData('location_id', id)} + error={errors.location_id} + /> +
+ + %
} + name="discount" + value={data.discount} + onChange={handleOnChange} + error={errors.discount} + formClassName={'pr-10'} + label="Discount" + max={100} + min={0} + /> +
+ +
+ +
+ +
+
+
+