diff --git a/TODO.md b/TODO.md index bc267c7..77419d0 100644 --- a/TODO.md +++ b/TODO.md @@ -13,13 +13,20 @@ - [x] Setting Level Customer (view levels, edit name of level,minimal saldo, max amount saldo, max hutang) - [x] Deposit Menu (view daftar histori deposit) - [x] Manual Approve Deposit -- [ ] Setting Bonus Coin (memasukan amount bonus coin yang didapat dengan level dan harga voucher) - coin rewards -- [ ] View Customer Coin History - [ ] List Customer Verification -- [ ] Manual Approve Verification +- [ ] Manual Approve Verification -> mendapatkan limit hutang +- [ ] Setting Bonus Coin (memasukan amount bonus coin yang didapat dengan level dan harga voucher) - bonus coin - [ ] Voucher Sales - [ ] Dashboard (gafik hasil penjualan : disorting tanggal, lokasi dan customer) - [ ] Notification (manual deposit, stock voucher) +- [ ] View Customer Coin History +- [ ] Voucher - harga per level +- [ ] Voucher - harga coin + +### Adds + +- hutang (paylater) adalah limit tiap customer jika deposit kurang dalam pembayaran voucher , setiap limit yang digunakan akan di potong / di lunasi ketika melakukan topup deposit +- tukar coin adalah dengan menambahkan harga coin di voucher dan menambahkan 1 fitur di customer untuk explorer voucher yang memiliki harga coin, disimpan menjadi sale biasa dengan cara 1 kali penukaran adalah 1 voucher ### Customer @@ -36,4 +43,6 @@ - [x] Register Refferal - [x] Customer View Coin History - [ ] Verified Akun +- [ ] Paylater +- [ ] Coin Explorer - [ ] Notification (purchase success, deposit success) diff --git a/app/Http/Controllers/Customer/CartController.php b/app/Http/Controllers/Customer/CartController.php index 3c6d597..139a787 100644 --- a/app/Http/Controllers/Customer/CartController.php +++ b/app/Http/Controllers/Customer/CartController.php @@ -96,25 +96,20 @@ class CartController extends Controller DB::beginTransaction(); $carts = collect(session('carts')); + if ($carts->count() == 0) { + return redirect()->route('home.index') + ->with('message', ['type' => 'error', 'message' => 'transaksi gagal, keranjang anda kosong']); + } + // validate voucher is available - $vouchers = Voucher::whereIn('id', $carts->pluck('id')->toArray())->get(); - $carts = $carts->map(function ($item) use ($vouchers) { - $voucher = $vouchers->firstWhere('id', $item['id']); - if ($voucher->is_sold == Voucher::SOLD) { - $voucher = $voucher->shuffle_unsold(); - // rare happen - if ($voucher == null) { - session()->remove('carts'); - return redirect()->route('home.index') - ->with('message', ['type' => 'error', 'message' => 'transaksi gagal, voucher sedang tidak tersedia']); - } + foreach ($carts as $item) { + $batchCount = $item['voucher']->count_unsold(); + if ($batchCount < $item['quantity']) { + session()->remove('carts'); + return redirect()->route('home.index') + ->with('message', ['type' => 'error', 'message' => 'transaksi gagal, voucher sedang tidak tersedia']); } - - return [ - ...$item, - 'voucher' => $voucher - ]; - }); + }; $total = $carts->sum(function ($item) { return $item['quantity'] * $item['voucher']->price; @@ -129,15 +124,18 @@ class CartController extends Controller ]); foreach ($carts as $item) { - $sale->items()->create([ - 'entity_type' => $item['voucher']::class, - 'entity_id' => $item['voucher']->id, - 'price' => $item['voucher']->price, - 'quantity' => $item['quantity'], - 'additional_info_json' => json_encode($item), - ]); - - $item['voucher']->update(['is_sold' => Voucher::SOLD]); + foreach (range(1, $item['quantity']) as $q) { + $voucher = $item['voucher']->shuffle_unsold(); + $sale->items()->create([ + 'entity_type' => $voucher::class, + 'entity_id' => $voucher->id, + 'price' => $voucher->price, + 'quantity' => 1, + 'additional_info_json' => json_encode($item), + ]); + + $voucher->update(['is_sold' => Voucher::SOLD]); + } } $deposit = $customer->deposites()->create([ diff --git a/app/Http/Controllers/Customer/VerificationController.php b/app/Http/Controllers/Customer/VerificationController.php new file mode 100644 index 0000000..164b89c --- /dev/null +++ b/app/Http/Controllers/Customer/VerificationController.php @@ -0,0 +1,32 @@ +validate(['image' => 'required|image']); + + $customer = Customer::where('id', auth()->id())->first(); + + $file = $request->file('image'); + $file->store('uploads/KTP', 'public'); + $customer->update([ + 'identity_image' => $file->hashName('uploads/KTP'), + 'identity_verified' => Customer::IN_VERICATION, + ]); + + return redirect()->route('customer.verification') + ->with('message', ['type' => 'success', 'message' => 'verification is in progress']); + } +} diff --git a/app/Http/Controllers/VerificationController.php b/app/Http/Controllers/VerificationController.php new file mode 100644 index 0000000..4a27b9b --- /dev/null +++ b/app/Http/Controllers/VerificationController.php @@ -0,0 +1,42 @@ +where('identity_verified', Customer::IN_VERICATION) + ->orderBy('updated_at', 'desc'); + + return inertia('Verification/Index', [ + 'query' => $query->paginate() + ]); + } + + public function edit(Customer $customer) + { + $levels = [ + CustomerLevel::where('key', CustomerLevel::GOLD)->first(), + CustomerLevel::where('key', CustomerLevel::PLATINUM)->first(), + ]; + + return inertia('Verification/Form', [ + 'customer' => $customer->load(['level']), + 'levels' => $levels + ]); + } + + public function update(Request $request, Customer $customer) + { + // TODO: here + $request->validate([]); + // + $customer->update([]); + } +} diff --git a/app/Http/Middleware/HandleInertiaCustomerRequests.php b/app/Http/Middleware/HandleInertiaCustomerRequests.php index 9f6998c..4595f7b 100644 --- a/app/Http/Middleware/HandleInertiaCustomerRequests.php +++ b/app/Http/Middleware/HandleInertiaCustomerRequests.php @@ -40,7 +40,7 @@ class HandleInertiaCustomerRequests extends Middleware return array_merge(parent::share($request), [ 'app_name' => env('APP_NAME', 'App Name'), 'auth' => [ - 'user' => auth('customer')->user()?->load(['level']), + 'user' => auth('customer')->user()?->load(['level', 'paylater']), ], 'flash' => [ 'message' => fn () => $request->session()->get('message') ?? ['type' => null, 'message' => null], diff --git a/app/Models/CoinHistory.php b/app/Models/CoinHistory.php index c9efbda..265186b 100644 --- a/app/Models/CoinHistory.php +++ b/app/Models/CoinHistory.php @@ -41,10 +41,10 @@ class CoinHistory extends Model { return Attribute::make(get: function () { if ($this->credit == 0) { - return 'Rp' . number_format($this->debit, 0, ',', '.'); + return number_format($this->debit, 0, ',', '.'); } - return '-Rp' . number_format($this->credit, 0, ',', '.'); + return number_format($this->credit, 0, ',', '.'); }); } diff --git a/app/Models/Customer.php b/app/Models/Customer.php index 3254ea8..0eed2c8 100644 --- a/app/Models/Customer.php +++ b/app/Models/Customer.php @@ -33,7 +33,6 @@ class Customer extends Authenticatable 'google_id', 'deposit_balance', 'coin_balance', - 'paylater_balance', 'identity_verified', 'identity_image', 'customer_level_id', @@ -47,9 +46,11 @@ class Customer extends Authenticatable protected $appends = [ 'image_url', + 'identity_image_url', 'display_deposit', 'display_coin', 'display_phone', + 'paylater_limit', ]; protected static function booted(): void @@ -103,6 +104,19 @@ class Customer extends Authenticatable ); } + public function identityImageUrl(): Attribute + { + return Attribute::make( + get: function () { + if ($this->identity_image != null) { + return asset($this->identity_image); + } + + return asset('sample/ktp_placeholder.png'); + } + ); + } + public function displayPhone(): Attribute { return Attribute::make(get: function () { @@ -128,6 +142,13 @@ class Customer extends Authenticatable }); } + public function paylaterLimit(): Attribute + { + return Attribute::make(get: function () { + return $this->paylater?->limit ?? ''; + }); + } + public function level() { return $this->belongsTo(CustomerLevel::class, 'customer_level_id'); @@ -148,6 +169,16 @@ class Customer extends Authenticatable return $this->hasMany(CoinHistory::class); } + public function paylater() + { + return $this->hasOne(PaylaterCustomer::class); + } + + public function paylaterHistories() + { + return $this->hasMany(PaylaterHistory::class); + } + public function customerRefferals() { return $this->hasMany(CustomerRefferal::class); diff --git a/app/Models/PaylaterCustomer.php b/app/Models/PaylaterCustomer.php new file mode 100644 index 0000000..129540c --- /dev/null +++ b/app/Models/PaylaterCustomer.php @@ -0,0 +1,13 @@ +batch_id] + ])->count(); + + return $voucher; + } } 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 29b9593..6a668e5 100644 --- a/database/migrations/2023_05_24_130522_create_vouchers_table.php +++ b/database/migrations/2023_05_24_130522_create_vouchers_table.php @@ -19,6 +19,7 @@ return new class extends Migration $table->ulid('location_id')->nullable(); $table->string('username')->nullable(); $table->string('password')->nullable(); + $table->decimal('price_coin', 20, 2)->default(0); $table->decimal('price', 20, 2)->default(0); $table->decimal('discount', 20, 0)->default(0); $table->decimal('display_price', 20, 2)->default(0); diff --git a/database/migrations/2023_05_24_130552_create_customers_table.php b/database/migrations/2023_05_24_130552_create_customers_table.php index c783247..3f916af 100644 --- a/database/migrations/2023_05_24_130552_create_customers_table.php +++ b/database/migrations/2023_05_24_130552_create_customers_table.php @@ -26,8 +26,7 @@ return new class extends Migration $table->string('google_id')->nullable(); $table->decimal('deposit_balance', 20, 2)->default(0); $table->decimal('coin_balance', 20, 2)->default(0); - $table->decimal('paylater_balance', 20, 2)->default(0); - $table->smallInteger('identity_verified')->nullable(); + $table->smallInteger('identity_verified')->default(0); $table->string('identity_image')->nullable(); $table->ulid('customer_level_id')->nullable(); $table->text('google_oauth_response')->nullable(); diff --git a/database/migrations/2023_06_04_234950_create_paylater_customers_table.php b/database/migrations/2023_06_04_234950_create_paylater_customers_table.php new file mode 100644 index 0000000..f907dde --- /dev/null +++ b/database/migrations/2023_06_04_234950_create_paylater_customers_table.php @@ -0,0 +1,37 @@ +ulid('id')->primary(); + + $table->text('description')->nullable(); + $table->ulid('customer_id')->nullable(); + $table->decimal('limit', 20, 2)->default(0); + $table->decimal('usage', 20, 2)->default(0); + + $table->timestamps(); + $table->softDeletes(); + $table->ulid('created_by')->nullable(); + $table->ulid('updated_by')->nullable(); + $table->ulid('deleted_by')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('paylater_customers'); + } +}; diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php index 91f38f8..8b0d2de 100644 --- a/database/seeders/PermissionSeeder.php +++ b/database/seeders/PermissionSeeder.php @@ -54,10 +54,7 @@ class PermissionSeeder extends Seeder ['id' => Str::ulid(), 'label' => 'Update Customer Level', 'name' => 'update-customer-level'], ['id' => Str::ulid(), 'label' => 'View Customer Level', 'name' => 'view-customer-level'], - ['id' => Str::ulid(), 'label' => 'Create Customer Verification', 'name' => 'create-customer-verification'], - ['id' => Str::ulid(), 'label' => 'Update Customer Verification', 'name' => 'update-customer-verification'], ['id' => Str::ulid(), 'label' => 'View Customer Verification', 'name' => 'view-customer-verification'], - ['id' => Str::ulid(), 'label' => 'Delete Customer Verification', 'name' => 'delete-customer-verification'], ['id' => Str::ulid(), 'label' => 'View Setting', 'name' => 'view-setting'], diff --git a/public/sample/ktp_placeholder.png b/public/sample/ktp_placeholder.png new file mode 100644 index 0000000..49a7b85 Binary files /dev/null and b/public/sample/ktp_placeholder.png differ diff --git a/resources/js/Customer/Cart/Index.jsx b/resources/js/Customer/Cart/Index.jsx index 61b5914..5515590 100644 --- a/resources/js/Customer/Cart/Index.jsx +++ b/resources/js/Customer/Cart/Index.jsx @@ -30,7 +30,7 @@ export default function Index({ auth: { user }, carts, total }) { return ( - +
Keranjang
diff --git a/resources/js/Customer/Coin/Index.jsx b/resources/js/Customer/Coin/Index.jsx index 0c98f9c..44611da 100644 --- a/resources/js/Customer/Coin/Index.jsx +++ b/resources/js/Customer/Coin/Index.jsx @@ -36,7 +36,7 @@ export default function Index({ Coin
- Rp {user.display_coin} + {user.display_coin}
diff --git a/resources/js/Customer/Index/BalanceBanner.jsx b/resources/js/Customer/Index/BalanceBanner.jsx index 2cc9cd5..e021457 100644 --- a/resources/js/Customer/Index/BalanceBanner.jsx +++ b/resources/js/Customer/Index/BalanceBanner.jsx @@ -25,7 +25,7 @@ export default function BalanceBanner({ user }) {
{user.level.name} Member
-
Limit 100.000
+
{user.paylater_limit}
diff --git a/resources/js/Customer/Index/Banner.jsx b/resources/js/Customer/Index/Banner.jsx index 86e8177..b5ad9e6 100644 --- a/resources/js/Customer/Index/Banner.jsx +++ b/resources/js/Customer/Index/Banner.jsx @@ -6,7 +6,7 @@ import { HiChevronLeft } from 'react-icons/hi2' export default function Banner({ banner }) { return ( - +
} /> diff --git a/resources/js/Customer/Profile/Index.jsx b/resources/js/Customer/Profile/Index.jsx index 2c96dca..5860dac 100644 --- a/resources/js/Customer/Profile/Index.jsx +++ b/resources/js/Customer/Profile/Index.jsx @@ -84,7 +84,17 @@ export default function Index({ auth: { user } }) {
+ {/* menu hanya muncul untuk member gold atau platinum */}
+
Paylater
+ +
+
+ router.get(route('customer.verification')) + } + >
Upgrade Member
@@ -101,7 +111,7 @@ export default function Index({ auth: { user } }) { className="flex flex-row justify-between items-center px-2 py-4 w-full border-b border-gray-400 hover:bg-gray-100" onClick={() => router.get(route('customer.coin.index'))} > -
Coin
+
Riwayat Coin
Transaksi #{sale.code}
-
{sale.format_created_at}
+
{sale.format_created_at}
+
+
+
TOTAL
+
{sale.display_amount}
+
+
{sale.items.map((item) => ( ))}
-
-
-
TOTAL
-
{sale.display_amount}
-
-
) diff --git a/resources/js/Customer/Trx/VoucherCard.jsx b/resources/js/Customer/Trx/VoucherCard.jsx index 50b076b..335ae20 100644 --- a/resources/js/Customer/Trx/VoucherCard.jsx +++ b/resources/js/Customer/Trx/VoucherCard.jsx @@ -64,12 +64,6 @@ export default function VoucherCard(props) {
-
-
{formatIDR(voucher.price)}
-
x
-
{quantity}
-
{formatIDR(+voucher.price * +quantity)}
-
diff --git a/resources/js/Customer/Verification/Index.jsx b/resources/js/Customer/Verification/Index.jsx new file mode 100644 index 0000000..6e98d37 --- /dev/null +++ b/resources/js/Customer/Verification/Index.jsx @@ -0,0 +1,102 @@ +import React, { useState } from 'react' +import { Head, useForm, router, usePage } from '@inertiajs/react' +import CustomerLayout from '@/Layouts/CustomerLayout' +import FormFile from '@/Components/FormFile' +import { HiCheckCircle, HiClock } from 'react-icons/hi2' + +// 0 yuk verifikasi +const VerificationForm = () => { + const { + props: { + auth: { user }, + }, + } = usePage() + const [show, setShow] = useState(false) + + const { setData, post, errors } = useForm({ image: null }) + + const handleSubmit = () => { + post(route('customer.verification')) + } + + return ( + <> +
+ upgrade akun kamu untuk mendapatkan banefit lebih, cukup lakukan + verifikasi data dengan upload foto KTP kamu +
+ {show === false ? ( +
setShow(true)} + > +
+ Verifikasi +
+
+ ) : ( +
+ setData('image', e.target.files[0])} + error={errors.image} + preview={ + ktp image + } + /> + +
handleSubmit()} + > + Upload +
+
+ )} + + ) +} + +// 1 verified +const Verified = () => { + return ( +
+ +
+ Akun anda berhasil terverifikasi +
+
+ ) +} + +// 2 your data is in review +const VerificationInProgress = () => { + return ( +
+ +
+ Data anda sedang kami proses untuk verifikasi +
+
+ ) +} + +export default function Index({ auth: { user } }) { + return ( + + +
+
+ Upgrade Member +
+ {+user.identity_verified === 0 && } + {+user.identity_verified === 1 && } + {+user.identity_verified === 2 && } +
+
+ ) +} diff --git a/resources/js/Layouts/CustomerLayout.jsx b/resources/js/Layouts/CustomerLayout.jsx index 32e8329..f075dbf 100644 --- a/resources/js/Layouts/CustomerLayout.jsx +++ b/resources/js/Layouts/CustomerLayout.jsx @@ -6,6 +6,8 @@ import { HiOutlineHome, HiOutlineUserCircle } from 'react-icons/hi' import { HiArrowPathRoundedSquare, HiBars3, + HiGift, + HiOutlineGift, HiOutlineShoppingCart, } from 'react-icons/hi2' @@ -51,7 +53,6 @@ export default function CustomerLayout({ children }) {
Beranda
-
Keranjang
+
handleOnClick('cart.index')} + > +
+ +
+
Coin
+
- +
diff --git a/resources/js/Pages/DepositHistory/Index.jsx b/resources/js/Pages/DepositHistory/Index.jsx index 9c1282e..e29552e 100644 --- a/resources/js/Pages/DepositHistory/Index.jsx +++ b/resources/js/Pages/DepositHistory/Index.jsx @@ -14,7 +14,7 @@ import SearchInput from '@/Components/SearchInput' import { formatIDR, hasPermission } from '@/utils' import { HiEye } from 'react-icons/hi2' -export default function Info(props) { +export default function Index(props) { const { query: { links, data }, auth, @@ -125,31 +125,20 @@ export default function Info(props) { > {deposit.status.text} - ))} @@ -157,7 +146,7 @@ export default function Info(props) {
Customer - - {canUpdate && ( - - toggleFormModal( - deposit - ) - } - > -
- -
- Lihat -
-
-
- )} -
+
+ {canUpdate && ( +
+ toggleFormModal( + deposit + ) + } + > + +
Lihat
+
+ )}
- +
diff --git a/resources/js/Pages/Verification/Form.jsx b/resources/js/Pages/Verification/Form.jsx new file mode 100644 index 0000000..dc60ffe --- /dev/null +++ b/resources/js/Pages/Verification/Form.jsx @@ -0,0 +1,102 @@ +import React, { useEffect, useState } from 'react' +import { useForm, Head, Link } from '@inertiajs/react' +import { isEmpty } from 'lodash' + +import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout' +import Button from '@/Components/Button' + +export default function Form(props) { + const { customer, levels } = props + const { data, setData, post, processing, errors, reset, clearErrors } = + useForm({ + level: '', + image_prove_url: '', + }) + + const handleOnChange = (event) => { + setData( + event.target.name, + event.target.type === 'checkbox' + ? event.target.checked + ? 1 + : 0 + : event.target.value + ) + } + + const handleSubmit = () => { + post(route('customer-verification.update', customer)) + } + + return ( + + + +
+
+
+
+ Verifikasi Customer +
+ + + + + + + + +
Customer: + + {customer.name} + +
+ +
+ +
+ +
+
Level Upgrade
+ +
+
+ +
+
+
+
+
+ ) +} diff --git a/resources/js/Pages/Verification/Index.jsx b/resources/js/Pages/Verification/Index.jsx new file mode 100644 index 0000000..017526c --- /dev/null +++ b/resources/js/Pages/Verification/Index.jsx @@ -0,0 +1,163 @@ +import React, { useEffect, useState } from 'react' +import { router } from '@inertiajs/react' +import { usePrevious } from 'react-use' +import { Head } from '@inertiajs/react' +import { Dropdown } from 'flowbite-react' +import { HiEye } from 'react-icons/hi2' + +import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout' +import Pagination from '@/Components/Pagination' +import SearchInput from '@/Components/SearchInput' + +export default function Index(props) { + const { + query: { links, data }, + auth, + } = props + + const [search, setSearch] = useState('') + const preValue = usePrevious(search) + + const params = { q: search } + useEffect(() => { + if (preValue) { + router.get( + route(route().current()), + { q: search }, + { + replace: true, + preserveState: true, + } + ) + } + }, [search]) + + return ( + + + +
+
+
+
+
+ setSearch(e.target.value)} + value={search} + /> +
+
+
+
+ + + + + + + + + + + + {data.map((customer) => ( + + + + + + + + + ))} + +
+ Nama + + Level + + Deposit + + Coin + + Referal Code + +
+ {customer.name} + + {customer.level.name} + + {customer.display_deposit} + + {customer.display_coin} + + {customer.referral_code} + +
{ + router.get( + route( + 'customer-verification.edit', + customer + ) + ) + }} + > + +
Lihat
+
+
+
+
+ +
+
+
+
+
+
+ ) +} diff --git a/routes/admin.php b/routes/admin.php index 3a8fc61..5480cb4 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Auth\AuthenticatedSessionController; use App\Http\Controllers\BannerController; use App\Http\Controllers\CustomerController; use App\Http\Controllers\CustomerLevelController; +use App\Http\Controllers\VerificationController; use App\Http\Controllers\DepositController; use App\Http\Controllers\GeneralController; use App\Http\Controllers\InfoController; @@ -83,6 +84,11 @@ Route::middleware(['http_secure_aware', 'inertia.admin']) Route::get('/customer-levels', [CustomerLevelController::class, 'index'])->name('customer-level.index'); Route::put('/customer-levels/{customerLevel}', [CustomerLevelController::class, 'update'])->name('customer-level.update'); + // verification + Route::get('/customers-verifications', [VerificationController::class, 'index'])->name('customer-verification.index'); + Route::get('/customers-verifications/{customer}', [VerificationController::class, 'edit'])->name('customer-verification.edit'); + Route::post('/customers-verifications/{customer}', [VerificationController::class, 'update'])->name('customer-verification.update'); + // customer Route::get('/customers', [CustomerController::class, 'index'])->name('customer.index'); Route::get('/customers/create', [CustomerController::class, 'create'])->name('customer.create'); diff --git a/routes/web.php b/routes/web.php index d8f3799..7862cca 100644 --- a/routes/web.php +++ b/routes/web.php @@ -7,6 +7,7 @@ use App\Http\Controllers\Customer\DepositController; use App\Http\Controllers\Customer\HomeController; use App\Http\Controllers\Customer\ProfileController; use App\Http\Controllers\Customer\TransactionController; +use App\Http\Controllers\Customer\VerificationController; use Illuminate\Support\Facades\Route; /* @@ -30,6 +31,11 @@ Route::middleware(['http_secure_aware', 'guard_should_customer', 'inertia.custom Route::get('profile', [ProfileController::class, 'index'])->name('customer.profile.index'); Route::get('profile/update', [ProfileController::class, 'show'])->name('customer.profile.show'); Route::post('profile/update', [ProfileController::class, 'update']); + + // verification + Route::get('profile/verification', [VerificationController::class, 'index'])->name('customer.verification'); + Route::post('profile/verification', [VerificationController::class, 'update']); + // logout Route::post('logout', [AuthController::class, 'destroy'])->name('customer.logout');