customer purchase done

dev
Aji Kamaludin 1 year ago
parent 2f9cd2994c
commit 7e0887015b
No known key found for this signature in database
GPG Key ID: 19058F67F0083AD3

@ -31,9 +31,9 @@
- [x] Customer Edit Profile - [x] Customer Edit Profile
- [x] Customer Deposit Manual - [x] Customer Deposit Manual
- [x] Customer Deposit Payment Gateway - [x] Customer Deposit Payment Gateway
- [ ] Customer Purchase Voucher - [x] Customer Purchase Voucher
- [ ] Register Refferal
- [ ] Customer Share Buyed Voucher, via WA dll - [ ] Customer Share Buyed Voucher, via WA dll
- [ ] Register Refferal
- [ ] Customer View Coin History - [ ] Customer View Coin History
- [ ] Verified Akun - [ ] Verified Akun
- [ ] Notification (purchase success, deposit success) - [ ] Notification (purchase success, deposit success)

@ -12,7 +12,7 @@ class AccountController extends Controller
$query = Account::orderBy('updated_at', 'desc')->paginate(); $query = Account::orderBy('updated_at', 'desc')->paginate();
return inertia('Account/Index', [ return inertia('Account/Index', [
'query' => $query 'query' => $query,
]); ]);
} }

@ -12,7 +12,7 @@ class BannerController extends Controller
$query = Banner::orderBy('updated_at', 'desc')->paginate(); $query = Banner::orderBy('updated_at', 'desc')->paginate();
return inertia('Banner/Index', [ return inertia('Banner/Index', [
'query' => $query 'query' => $query,
]); ]);
} }
@ -45,7 +45,7 @@ class BannerController extends Controller
public function edit(Banner $banner) public function edit(Banner $banner)
{ {
return inertia('Banner/Form', [ return inertia('Banner/Form', [
'banner' => $banner 'banner' => $banner,
]); ]);
} }

@ -4,8 +4,6 @@ namespace App\Http\Controllers\Customer;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Customer; use App\Models\Customer;
use App\Models\CustomerLevel;
use App\Models\CustomerLevelHistory;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@ -72,7 +70,7 @@ class AuthController extends Controller
'fullname' => $user->name, 'fullname' => $user->name,
'name' => $user->nickname, 'name' => $user->nickname,
'email' => $user->email, 'email' => $user->email,
'username' => Str::slug($user->name . '_' . Str::random(5), '_'), 'username' => Str::slug($user->name.'_'.Str::random(5), '_'),
'google_id' => $user->id, 'google_id' => $user->id,
'google_oauth_response' => json_encode($user), 'google_oauth_response' => json_encode($user),
]); ]);

@ -0,0 +1,159 @@
<?php
namespace App\Http\Controllers\Customer;
use App\Http\Controllers\Controller;
use App\Models\Customer;
use App\Models\DepositHistory;
use App\Models\Sale;
use App\Models\Voucher;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class CartController extends Controller
{
/**
* show list of item in cart
* has payed button
* show payment method -> deposit, coin, paylater
*
*/
public function index()
{
$carts = collect(session('carts') ?? []);
$total = $carts->sum(function ($item) {
return $item['quantity'] * $item['voucher']->price;
});
return inertia('Home/Cart/Index', [
'carts' => $carts,
'total' => $total,
]);
}
/**
* handle cart add, remove or sub
*
*/
public function store(Request $request, Voucher $voucher)
{
$operator = $request->param ?? 'add';
$voucher->load(['location']);
$carts = collect(session('carts') ?? []);
if ($carts->count() > 0) {
$item = $carts->firstWhere('id', $voucher->id);
if ($item == null) {
$carts->add(['id' => $voucher->id, 'quantity' => 1, 'voucher' => $voucher]);
session(['carts' => $carts->toArray()]);
session()->flash('message', ['type' => 'success', 'message' => 'voucher added to cart']);
} else {
$carts = $carts->map(function ($item) use ($voucher, $operator) {
if ($item['id'] == $voucher->id) {
if ($operator == 'delete') {
return ['id' => null];
}
if ($operator == 'add') {
$quantity = $item['quantity'] + 1;
}
if ($operator == 'sub') {
$quantity = $item['quantity'] - 1;
if ($quantity <= 0) {
$quantity = 1;
}
}
return [
...$item,
'quantity' => $quantity,
];
}
return $item;
});
$carts = $carts->whereNotNull('id')->toArray();
session(['carts' => $carts]);
}
return;
}
session(['carts' => [
['id' => $voucher->id, 'quantity' => 1, 'voucher' => $voucher],
]]);
session()->flash('message', ['type' => 'success', 'message' => 'voucher added to cart']);
}
/**
* find correct voucher , reject if cant be found
* create sale and item sale
* credit deposit
* redirect to show detail
*/
public function purchase()
{
DB::beginTransaction();
$carts = collect(session('carts'));
// 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']);
}
}
return [
...$item,
'voucher' => $voucher
];
});
$total = $carts->sum(function ($item) {
return $item['quantity'] * $item['voucher']->price;
});
$customer = Customer::find(auth()->id());
$sale = $customer->sales()->create([
'code' => Str::random(5),
'date_time' => now(),
'amount' => $total,
'payed_with' => Sale::PAYED_WITH_DEPOSIT,
]);
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]);
}
$deposit = $customer->deposites()->create([
'credit' => $total,
'description' => 'Pembayaran #' . $sale->code,
'related_type' => $sale::class,
'related_id' => $sale->id,
'is_valid' => DepositHistory::STATUS_VALID,
]);
$deposit->update_customer_balance();
DB::commit();
session()->remove('carts');
return redirect()->route('transactions.show', $sale)
->with('message', ['type' => 'success', 'message' => 'pembelian berhasil']);
}
}

@ -22,7 +22,7 @@ class DepositController extends Controller
->orderBy('is_valid', 'desc'); ->orderBy('is_valid', 'desc');
return inertia('Home/Deposit/Index', [ return inertia('Home/Deposit/Index', [
'histories' => $histories->paginate(20) 'histories' => $histories->paginate(20),
]); ]);
} }
@ -39,15 +39,15 @@ class DepositController extends Controller
'amount' => 'required|numeric|min:10000', 'amount' => 'required|numeric|min:10000',
'payment' => [ 'payment' => [
'required', 'required',
Rule::in([Setting::PAYMENT_MANUAL, Setting::PAYMENT_MIDTRANS]) Rule::in([Setting::PAYMENT_MANUAL, Setting::PAYMENT_MIDTRANS]),
] ],
]); ]);
DB::beginTransaction(); DB::beginTransaction();
$deposit = DepositHistory::make([ $deposit = DepositHistory::make([
'customer_id' => auth()->id(), 'customer_id' => auth()->id(),
'debit' => $request->amount, 'debit' => $request->amount,
'description' => 'Top Up #' . Str::random(5), 'description' => 'Top Up #'.Str::random(5),
'payment_channel' => $request->payment, 'payment_channel' => $request->payment,
]); ]);
@ -77,7 +77,7 @@ class DepositController extends Controller
'accounts' => Account::get(), 'accounts' => Account::get(),
'midtrans_client_key' => Setting::getByKey('MIDTRANS_CLIENT_KEY'), 'midtrans_client_key' => Setting::getByKey('MIDTRANS_CLIENT_KEY'),
'is_production' => app()->isProduction(), 'is_production' => app()->isProduction(),
'direct' => $request->direct 'direct' => $request->direct,
]); ]);
} }
@ -93,10 +93,10 @@ class DepositController extends Controller
$deposit->update([ $deposit->update([
'image_prove' => $file->hashName('uploads'), 'image_prove' => $file->hashName('uploads'),
'account_id' => $request->account_id, 'account_id' => $request->account_id,
'is_valid' => DepositHistory::STATUS_WAIT_APPROVE 'is_valid' => DepositHistory::STATUS_WAIT_APPROVE,
]); ]);
session()->flash('message', ['type' => 'success', 'message' => 'Upload berhasil, silahkan tunggu untuk approve']);; session()->flash('message', ['type' => 'success', 'message' => 'Upload berhasil, silahkan tunggu untuk approve']);
} }
public function midtrans_payment(Request $request, DepositHistory $deposit) public function midtrans_payment(Request $request, DepositHistory $deposit)

@ -17,6 +17,7 @@ class HomeController extends Controller
$banners = Banner::orderBy('updated_at', 'desc')->get(); $banners = Banner::orderBy('updated_at', 'desc')->get();
$locations = Location::get(); $locations = Location::get();
$vouchers = Voucher::with(['location']) $vouchers = Voucher::with(['location'])
->where('is_sold', Voucher::UNSOLD)
->groupBy('batch_id') ->groupBy('batch_id')
->orderBy('updated_at', 'desc'); ->orderBy('updated_at', 'desc');
@ -28,8 +29,8 @@ class HomeController extends Controller
'infos' => $infos, 'infos' => $infos,
'banners' => $banners, 'banners' => $banners,
'locations' => $locations, 'locations' => $locations,
'vouchers' => $vouchers->paginate(10), 'vouchers' => tap($vouchers->paginate(10))->setHidden(['username', 'password']),
'_location_id' => $request->location_id ?? '' '_location_id' => $request->location_id ?? '',
]); ]);
} }

@ -26,7 +26,7 @@ class ProfileController extends Controller
'name' => 'string|required', 'name' => 'string|required',
'address' => 'string|required', 'address' => 'string|required',
'phone' => 'string|required|numeric', 'phone' => 'string|required|numeric',
'username' => 'string|required|min:5|alpha_dash|unique:customers,username,' . $customer->id, 'username' => 'string|required|min:5|alpha_dash|unique:customers,username,'.$customer->id,
'password' => 'nullable|string|min:8|confirmed', 'password' => 'nullable|string|min:8|confirmed',
'image' => 'nullable|image', 'image' => 'nullable|image',
]); ]);
@ -51,6 +51,6 @@ class ProfileController extends Controller
'password' => $customer->password, 'password' => $customer->password,
]); ]);
session()->flash('message', ['type' => 'success', 'message' => 'profile updateded']); session()->flash('message', ['type' => 'success', 'message' => 'profile updated']);
} }
} }

@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers\Customer;
use App\Http\Controllers\Controller;
use App\Models\Sale;
class TransactionController extends Controller
{
public function index()
{
$query = Sale::where('customer_id', auth()->id())
->orderBy('created_at', 'desc');
return inertia('Home/Trx/Index', [
'query' => $query->paginate(),
]);
}
public function show(Sale $sale)
{
return inertia('Home/Trx/Detail', [
'sale' => $sale->load(['items.voucher.location'])
]);
}
}

@ -57,14 +57,14 @@ class CustomerController extends Controller
public function edit(Customer $customer) public function edit(Customer $customer)
{ {
return inertia('Customer/Form', [ return inertia('Customer/Form', [
'customer' => $customer 'customer' => $customer,
]); ]);
} }
public function update(Request $request, Customer $customer) public function update(Request $request, Customer $customer)
{ {
$request->validate([ $request->validate([
'username' => 'required|string|min:5|alpha_dash|unique:customers,username,' . $customer->id, 'username' => 'required|string|min:5|alpha_dash|unique:customers,username,'.$customer->id,
'password' => 'nullable|string|min:8', 'password' => 'nullable|string|min:8',
'name' => 'required|string', 'name' => 'required|string',
'fullname' => 'required|string', 'fullname' => 'required|string',

@ -10,8 +10,9 @@ class CustomerLevelController extends Controller
public function index() public function index()
{ {
$query = CustomerLevel::query(); $query = CustomerLevel::query();
return inertia('CustomerLevel/Index', [ return inertia('CustomerLevel/Index', [
'query' => $query->paginate() 'query' => $query->paginate(),
]); ]);
} }
@ -21,7 +22,7 @@ class CustomerLevelController extends Controller
'name' => 'required|string', 'name' => 'required|string',
'description' => 'nullable|string', 'description' => 'nullable|string',
'min_amount' => 'required|numeric|min:0', 'min_amount' => 'required|numeric|min:0',
'max_amount' => 'required|numeric|min:0' 'max_amount' => 'required|numeric|min:0',
]); ]);
$customerLevel->update([ $customerLevel->update([

@ -36,7 +36,7 @@ class DepositController extends Controller
} }
return inertia('DepositHistory/Index', [ return inertia('DepositHistory/Index', [
'query' => $query->paginate() 'query' => $query->paginate(),
]); ]);
} }
@ -45,15 +45,15 @@ class DepositController extends Controller
$request->validate([ $request->validate([
'status' => [ 'status' => [
'required', 'required',
Rule::in([DepositHistory::STATUS_VALID, DepositHistory::STATUS_REJECT]) Rule::in([DepositHistory::STATUS_VALID, DepositHistory::STATUS_REJECT]),
] ],
]); ]);
DB::beginTransaction(); DB::beginTransaction();
$deposit->update([ $deposit->update([
'is_valid' => $request->status, 'is_valid' => $request->status,
]); ]);
if ($request->status === DepositHistory::STATUS_VALID) { if ($request->status == DepositHistory::STATUS_VALID) {
$deposit->update_customer_balance(); $deposit->update_customer_balance();
} }
DB::commit(); DB::commit();

@ -23,7 +23,6 @@ class GeneralController extends Controller
$file = $request->file('image'); $file = $request->file('image');
$file->store('uploads', 'public'); $file->store('uploads', 'public');
return response()->json([ return response()->json([
'id' => Str::ulid(), 'id' => Str::ulid(),
'name' => $file->getClientOriginalName(), 'name' => $file->getClientOriginalName(),

@ -14,7 +14,7 @@ class SettingController extends Controller
return inertia('Setting/Index', [ return inertia('Setting/Index', [
'setting' => $setting, 'setting' => $setting,
'midtrans_notification_url' => route('api.midtrans.notification') 'midtrans_notification_url' => route('api.midtrans.notification'),
]); ]);
} }

@ -21,7 +21,7 @@ class VoucherController extends Controller
} }
return inertia('Voucher/Index', [ return inertia('Voucher/Index', [
'query' => $query->paginate() 'query' => $query->paginate(),
]); ]);
} }

@ -14,11 +14,12 @@ class Authenticate extends Middleware
*/ */
protected function redirectTo($request) protected function redirectTo($request)
{ {
if (!$request->expectsJson()) { if (! $request->expectsJson()) {
$uri = $request->getRequestUri(); $uri = $request->getRequestUri();
if (str_contains($uri, 'admin')) { if (str_contains($uri, 'admin')) {
return route('admin.login'); return route('admin.login');
} }
return route('customer.login'); return route('customer.login');
} }
} }

@ -29,6 +29,14 @@ class HandleInertiaCustomerRequests extends Middleware
*/ */
public function share(Request $request): array public function share(Request $request): array
{ {
$carts = collect(session('carts') ?? []);
$cart_count = 0;
if ($carts->count() > 0) {
foreach ($carts as $cart) {
$cart_count += $cart['quantity'];
}
}
return array_merge(parent::share($request), [ return array_merge(parent::share($request), [
'app_name' => env('APP_NAME', 'App Name'), 'app_name' => env('APP_NAME', 'App Name'),
'auth' => [ 'auth' => [
@ -38,7 +46,7 @@ class HandleInertiaCustomerRequests extends Middleware
'message' => fn () => $request->session()->get('message') ?? ['type' => null, 'message' => null], 'message' => fn () => $request->session()->get('message') ?? ['type' => null, 'message' => null],
], ],
'notification_count' => 0, 'notification_count' => 0,
'carts' => [] 'cart_count' => $cart_count,
]); ]);
} }
} }

@ -15,7 +15,7 @@ class Banner extends Model
]; ];
protected $appends = [ protected $appends = [
'image_url' 'image_url',
]; ];
protected function imageUrl(): Attribute protected function imageUrl(): Attribute

@ -43,7 +43,7 @@ class Customer extends Authenticatable
'image_url', 'image_url',
'display_deposit', 'display_deposit',
'display_coin', 'display_coin',
'display_phone' 'display_phone',
]; ];
protected static function booted(): void protected static function booted(): void
@ -103,6 +103,7 @@ class Customer extends Authenticatable
if ($this->phone === null) { if ($this->phone === null) {
return ' - '; return ' - ';
} }
return '+62' . $this->phone; return '+62' . $this->phone;
}); });
} }
@ -125,4 +126,14 @@ class Customer extends Authenticatable
{ {
return $this->belongsTo(CustomerLevel::class, 'customer_level_id'); return $this->belongsTo(CustomerLevel::class, 'customer_level_id');
} }
public function sales()
{
return $this->hasMany(Sale::class);
}
public function deposites()
{
return $this->hasMany(DepositHistory::class);
}
} }

@ -41,7 +41,7 @@ class DepositHistory extends Model
'format_human_created_at', 'format_human_created_at',
'format_created_at', 'format_created_at',
'amount', 'amount',
'image_prove_url' 'image_prove_url',
]; ];
public function status(): Attribute public function status(): Attribute
@ -61,14 +61,14 @@ class DepositHistory extends Model
public function formatHumanCreatedAt(): Attribute public function formatHumanCreatedAt(): Attribute
{ {
return Attribute::make(get: function () { return Attribute::make(get: function () {
return Carbon::parse($this->created_at)->locale('id')->format('d F Y'); return Carbon::parse($this->created_at)->locale('id')->translatedFormat('d F Y');
}); });
} }
public function formatCreatedAt(): Attribute public function formatCreatedAt(): Attribute
{ {
return Attribute::make(get: function () { return Attribute::make(get: function () {
return Carbon::parse($this->created_at)->locale('id')->format('d M Y H:i:s'); return Carbon::parse($this->created_at)->locale('id')->translatedFormat('d F Y H:i:s');
}); });
} }
@ -78,6 +78,7 @@ class DepositHistory extends Model
if ($this->credit == 0) { if ($this->credit == 0) {
return 'Rp' . number_format($this->debit, 0, ',', '.'); return 'Rp' . number_format($this->debit, 0, ',', '.');
} }
return '-Rp' . number_format($this->credit, 0, ',', '.'); return '-Rp' . number_format($this->credit, 0, ',', '.');
}); });
} }
@ -102,6 +103,6 @@ class DepositHistory extends Model
public function update_customer_balance() public function update_customer_balance()
{ {
$customer = Customer::find($this->customer_id); $customer = Customer::find($this->customer_id);
$customer->update(['deposit_balance' => $customer->deposit_balance + $this->debit]); $customer->update(['deposit_balance' => $customer->deposit_balance + $this->debit - $this->credit]);
} }
} }

@ -2,6 +2,9 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Carbon;
class Sale extends Model class Sale extends Model
{ {
const PAYED_WITH_MIDTRANS = 'midtrans'; const PAYED_WITH_MIDTRANS = 'midtrans';
@ -13,6 +16,7 @@ class Sale extends Model
const PAYED_WITH_COIN = 'coin'; const PAYED_WITH_COIN = 'coin';
protected $fillable = [ protected $fillable = [
'code',
'customer_id', 'customer_id',
'date_time', 'date_time',
'amount', 'amount',
@ -23,4 +27,37 @@ class Sale extends Model
'payment_channel', 'payment_channel',
'payment_type', 'payment_type',
]; ];
protected $appends = [
'format_human_created_at',
'format_created_at',
'display_amount',
];
public function items()
{
return $this->hasMany(SaleItem::class);
}
public function formatHumanCreatedAt(): Attribute
{
return Attribute::make(get: function () {
return Carbon::parse($this->created_at)->locale('id')->translatedFormat('d F Y');
});
}
public function formatCreatedAt(): Attribute
{
return Attribute::make(get: function () {
return Carbon::parse($this->created_at)->locale('id')->translatedFormat('d F Y H:i:s');
});
}
public function displayAmount(): Attribute
{
return Attribute::make(get: function () {
return 'Rp' . number_format($this->amount, 0, ',', '.');
});
}
} }

@ -9,6 +9,17 @@ class SaleItem extends Model
'entity_type', 'entity_type',
'entity_id', 'entity_id',
'price', 'price',
'quantity',
'additional_info_json', 'additional_info_json',
]; ];
public function related()
{
return $this->belongsTo($this->entity_type, 'entity_id');
}
public function voucher()
{
return $this->belongsTo(Voucher::class, 'entity_id');
}
} }

@ -7,6 +7,10 @@ use Illuminate\Support\Str;
class Voucher extends Model class Voucher extends Model
{ {
const UNSOLD = 0;
const SOLD = 1;
protected $fillable = [ protected $fillable = [
'name', 'name',
'description', 'description',
@ -21,8 +25,8 @@ class Voucher extends Model
'comment', 'comment',
'expired', 'expired',
'expired_unit', 'expired_unit',
'is_sold', //menandakan sudah terjual atau belum 'is_sold', //menandakan sudah terjual atau belum
// batch pada saat import , jadi ketika user ingin beli akan tetapi sudah sold , // batch pada saat import , jadi ketika user ingin beli akan tetapi sudah sold ,
// maka akan dicarikan voucher lain dari batch yang sama // maka akan dicarikan voucher lain dari batch yang sama
'batch_id', 'batch_id',
]; ];
@ -73,4 +77,14 @@ class Voucher extends Model
{ {
return $this->belongsTo(Location::class)->withTrashed(); return $this->belongsTo(Location::class)->withTrashed();
} }
public function shuffle_unsold()
{
$voucher = Voucher::where([
['is_sold', '=', self::UNSOLD],
['batch_id', '=', $this->batch_id]
])->first();
return $voucher;
}
} }

@ -56,16 +56,21 @@ class GeneralService
public static function getEnablePayment() public static function getEnablePayment()
{ {
$payment = [ $payment = [
['name' => Setting::PAYMENT_MANUAL, 'logo' => null, 'display_name' => 'Transfer Manual'] ['name' => Setting::PAYMENT_MANUAL, 'logo' => null, 'display_name' => 'Transfer Manual'],
]; ];
$midtrans_enable = Setting::getByKey('MIDTRANS_ENABLED'); $midtrans_enable = Setting::getByKey('MIDTRANS_ENABLED');
if ($midtrans_enable == 1) { if ($midtrans_enable == 1) {
$payment[] = ['name' => Setting::PAYMENT_MIDTRANS, 'logo' => Setting::getByKey('MIDTRANS_LOGO')]; $payment[] = ['name' => Setting::PAYMENT_MIDTRANS, 'logo' => Setting::getByKey('MIDTRANS_LOGO')];
} }
// Paylater
return $payment; return $payment;
} }
public static function getCartEnablePayment()
{
// deposit
// coin
// paylater
}
} }

@ -40,8 +40,8 @@ class MidtransService
'address' => $this->deposit->customer->address, 'address' => $this->deposit->customer->address,
], ],
'callbacks' => [ 'callbacks' => [
'finish' => route('customer.deposit.show', ['deposit' => $this->deposit->id]) 'finish' => route('customer.deposit.show', ['deposit' => $this->deposit->id]),
] ],
]; ];
$snapToken = Snap::getSnapToken($params); $snapToken = Snap::getSnapToken($params);

@ -14,6 +14,7 @@ return new class extends Migration
Schema::create('sales', function (Blueprint $table) { Schema::create('sales', function (Blueprint $table) {
$table->ulid('id')->primary(); $table->ulid('id')->primary();
$table->string('code')->nullable();
$table->ulid('customer_id')->nullable(); $table->ulid('customer_id')->nullable();
$table->timestamp('date_time')->nullable(); $table->timestamp('date_time')->nullable();
$table->decimal('amount', 20, 2)->default(0); $table->decimal('amount', 20, 2)->default(0);

@ -18,6 +18,7 @@ return new class extends Migration
$table->string('entity_type')->nullable(); $table->string('entity_type')->nullable();
$table->ulid('entity_id')->nullable(); $table->ulid('entity_id')->nullable();
$table->decimal('price', 20, 2)->default(0); $table->decimal('price', 20, 2)->default(0);
$table->integer('quantity')->default(0);
$table->text('additional_info_json')->nullable(); $table->text('additional_info_json')->nullable();
$table->timestamps(); $table->timestamps();

@ -41,9 +41,9 @@ class DummySeeder extends Seeder
$images = ['1.webp', '2.webp', '3.webp']; $images = ['1.webp', '2.webp', '3.webp'];
foreach ($images as $index => $image) { foreach ($images as $index => $image) {
Banner::create([ Banner::create([
'title' => 'Banner ' . $index, 'title' => 'Banner '.$index,
'image' => 'sample/' . $image, 'image' => 'sample/'.$image,
'description' => '<h1>Banner </h1>' 'description' => '<h1>Banner </h1>',
]); ]);
} }
} }
@ -52,7 +52,7 @@ class DummySeeder extends Seeder
{ {
$banks = [ $banks = [
['name' => 'BRI', 'bank_name' => 'Bank Rakyat Indonesia', 'holder_name' => 'Aji Kamaludin', 'account_number' => '187391738129'], ['name' => 'BRI', 'bank_name' => 'Bank Rakyat Indonesia', 'holder_name' => 'Aji Kamaludin', 'account_number' => '187391738129'],
['name' => 'Jago', 'bank_name' => 'Bank Jago', 'holder_name' => 'Aji Kamaludin', 'account_number' => '718297389172'] ['name' => 'Jago', 'bank_name' => 'Bank Jago', 'holder_name' => 'Aji Kamaludin', 'account_number' => '718297389172'],
]; ];
foreach ($banks as $bank) { foreach ($banks as $bank) {
@ -72,7 +72,7 @@ class DummySeeder extends Seeder
foreach ($locations as $location) { foreach ($locations as $location) {
Location::create([ Location::create([
'name' => $location, 'name' => $location,
'description' => '-' 'description' => '-',
]); ]);
} }
} }
@ -82,7 +82,6 @@ class DummySeeder extends Seeder
$vouchers = GeneralService::script_parser(file_get_contents(public_path('example.md'))); $vouchers = GeneralService::script_parser(file_get_contents(public_path('example.md')));
DB::beginTransaction(); DB::beginTransaction();
foreach ([1, 2] as $loop) { foreach ([1, 2] as $loop) {
$batchId = Str::ulid(); $batchId = Str::ulid();

@ -1,4 +1,4 @@
import React from 'react' import React, { useEffect } from 'react'
import { ToastContainer, toast } from 'react-toastify' import { ToastContainer, toast } from 'react-toastify'
import { router, usePage } from '@inertiajs/react' import { router, usePage } from '@inertiajs/react'
@ -13,8 +13,11 @@ export default function CustomerLayout({ children }) {
const { const {
props: { props: {
auth: { user }, auth: { user },
cart_count,
flash,
}, },
} = usePage() } = usePage()
const handleOnClick = (r) => { const handleOnClick = (r) => {
router.get(route(r)) router.get(route(r))
} }
@ -27,6 +30,12 @@ export default function CustomerLayout({ children }) {
return 'text-gray-600' return 'text-gray-600'
} }
useEffect(() => {
if (flash.message !== null) {
toast(flash.message.message, { type: flash.message.type })
}
}, [flash])
return ( return (
<div className="min-h-screen flex flex-col sm:justify-center items-center"> <div className="min-h-screen flex flex-col sm:justify-center items-center">
<div className="flex flex-col w-full bg-white shadow pb-20 min-h-[calc(90dvh)] max-w-md"> <div className="flex flex-col w-full bg-white shadow pb-20 min-h-[calc(90dvh)] max-w-md">
@ -42,18 +51,29 @@ export default function CustomerLayout({ children }) {
<HiOutlineHome className="h-6 w-6" /> <HiOutlineHome className="h-6 w-6" />
<div className="text-xs font-light">Beranda</div> <div className="text-xs font-light">Beranda</div>
</div> </div>
<div className="py-2 px-5 hover:bg-blue-200 flex flex-col items-center text-gray-600">
<div
className={`pb-1 pt-2 px-5 hover:bg-blue-200 flex flex-col items-center ${isActive(
'cart.index'
)}`}
onClick={() => handleOnClick('cart.index')}
>
<div className="flex flex-row"> <div className="flex flex-row">
<HiOutlineShoppingCart className="h-6 w-6" /> <HiOutlineShoppingCart className="h-6 w-6" />
<div> <div>
<div className="bg-blue-300 text-blue-600 rounded-lg px-1 text-xs -ml-2"> <div className="bg-blue-300 text-blue-600 rounded-lg px-1 text-xs -ml-2">
1 {cart_count}
</div> </div>
</div> </div>
</div> </div>
<div className="text-xs font-light">Keranjang</div> <div className="text-xs font-light">Keranjang</div>
</div> </div>
<div className="py-2 px-5 hover:bg-blue-200 flex flex-col items-center text-gray-600"> <div
className={`pb-1 pt-2 px-5 hover:bg-blue-200 flex flex-col items-center ${isActive(
'transactions.*'
)}`}
onClick={() => handleOnClick('transactions.index')}
>
<HiArrowPathRoundedSquare className="h-6 w-6" /> <HiArrowPathRoundedSquare className="h-6 w-6" />
<div className="text-xs font-light">Transaksi</div> <div className="text-xs font-light">Transaksi</div>
</div> </div>

@ -22,7 +22,6 @@ export default function FormModal(props) {
}, },
payment_channel: '', payment_channel: '',
is_valid: 0, is_valid: 0,
status: '',
status_text: '', status_text: '',
text_color: '', text_color: '',
customer_name: '', customer_name: '',

@ -0,0 +1,80 @@
import React from 'react'
import { Head, router } from '@inertiajs/react'
import CustomerLayout from '@/Layouts/CustomerLayout'
import VoucherCard from './VoucherCard'
import { formatIDR } from '@/utils'
const EmptyHere = () => {
return (
<div className="w-full px-5 text-center flex flex-col my-auto">
<div className="font-bold text-xl">
Wah, keranjang belanjamu kosong
</div>
<div className="text-gray-400">
Yuk, pilih paket voucher terbaik mu!
</div>
</div>
)
}
export default function Index({ auth: { user }, carts, total }) {
const canPay = +user.deposit_balance >= +total
const handleSubmit = () => {
router.post(route('cart.purchase'))
}
const handleTopUp = () => {
router.get(route('customer.deposit.topup'))
}
return (
<CustomerLayout>
<Head title="Index" />
<div className="flex flex-col min-h-[calc(95dvh)]">
<div className="py-5 text-2xl px-5 font-bold">Keranjang</div>
{carts.length > 0 ? (
<>
<div className="w-full px-5 flex flex-col space-y-2">
{carts.map((item) => (
<VoucherCard key={item.id} item={item} />
))}
</div>
<div className="fixed bottom-20 right-0 w-full">
<div className="max-w-sm mx-auto text-right text-gray-400">
Saldo: {formatIDR(user.deposit_balance)}
</div>
<div className="max-w-sm mx-auto text-xl font-bold text-right flex flex-row justify-between">
<div>TOTAL</div>
<div> {formatIDR(total)}</div>
</div>
{canPay ? (
<div
onClick={handleSubmit}
className="mt-3 border bg-blue-700 text-white px-5 py-2 mx-auto rounded-full hover:text-black hover:bg-white max-w-sm"
>
Bayar
</div>
) : (
<div className="flex flex-row w-full mx-auto space-x-2 max-w-sm items-center mt-3">
<div className="border border-gray-500 bg-gray-400 text-white px-5 py-2 rounded-full flex-1">
Saldo tidak cukup
</div>
<div
onClick={handleTopUp}
className="border bg-blue-700 text-white px-5 py-2 rounded-full hover:text-black hover:bg-white"
>
Top Up
</div>
</div>
)}
</div>
</>
) : (
<EmptyHere />
)}
</div>
</CustomerLayout>
)
}

@ -0,0 +1,74 @@
import { formatIDR } from '@/utils'
import { router } from '@inertiajs/react'
import { HiMinusCircle, HiPlusCircle, HiTrash } from 'react-icons/hi2'
export default function VoucherCard({ item: { voucher, quantity } }) {
const handleDelete = () => {
router.post(route('cart.store', { voucher: voucher, param: 'delete' }))
}
const handleAdd = () => {
router.post(route('cart.store', { voucher: voucher, param: 'add' }))
}
const handleSub = () => {
router.post(route('cart.store', { voucher: voucher, param: 'sub' }))
}
return (
<div className="px-3 py-1 shadow-md rounded border border-gray-100">
<div className="text-base font-bold">{voucher.location.name}</div>
<div className="w-full border border-dashed"></div>
<div className="flex flex-row justify-between items-center">
<div>
<div className="text-xs text-gray-400 py-1">
{voucher.profile}
</div>
<div className="text-xl font-bold">
IDR {formatIDR(voucher.price)}
</div>
{+voucher.discount !== 0 && (
<div className="flex flex-row space-x-2 items-center text-xs pb-2">
<div className="bg-red-300 text-red-600 px-1 py-0.5 font-bold rounded">
{voucher.discount}%
</div>
<div className="text-gray-400 line-through">
{formatIDR(voucher.display_price)}
</div>
</div>
)}
</div>
<div className="flex flex-col justify-end">
<div className="text-3xl font-bold">
{voucher.display_quota}
</div>
<div className="text-gray-400">
{voucher.display_expired}
</div>
</div>
</div>
<div className="w-full border border-dashed"></div>
<div className="w-full flex flex-row justify-between items-center pt-1">
<div>{formatIDR(voucher.price)}</div>
<div>x</div>
<div>{quantity}</div>
<div>{formatIDR(+voucher.price * +quantity)}</div>
</div>
<div className="w-full flex flex-row justify-end items-center space-x-2 py-2">
<HiTrash
className="text-red-700 w-6 h-6 rounded-full border mr-4 hover:bg-red-700"
onClick={handleDelete}
/>
<HiPlusCircle
className="text-gray-400 w-6 h-6 rounded-full border hover:bg-gray-400"
onClick={handleAdd}
/>
<div>{quantity}</div>
<HiMinusCircle
className="text-gray-400 w-6 h-6 rounded-full border hover:bg-gray-400"
onClick={handleSub}
/>
</div>
</div>
)
}

@ -1,8 +1,15 @@
import { formatIDR } from '@/utils' import { formatIDR } from '@/utils'
import { router } from '@inertiajs/react'
export default function VoucherCard({ voucher }) { export default function VoucherCard({ voucher }) {
const addCart = () => {
router.post(route('cart.store', voucher))
}
return ( return (
<div className="px-3 py-1 shadow-md rounded border border-gray-100 hover:bg-gray-50"> <div
className="px-3 py-1 shadow-md rounded border border-gray-100 hover:bg-gray-50"
onClick={addCart}
>
<div className="text-base font-bold">{voucher.location.name}</div> <div className="text-base font-bold">{voucher.location.name}</div>
<div className="w-full border border-dashed"></div> <div className="w-full border border-dashed"></div>
<div className="flex flex-row justify-between items-center"> <div className="flex flex-row justify-between items-center">
@ -24,11 +31,11 @@ export default function VoucherCard({ voucher }) {
</div> </div>
)} )}
</div> </div>
<div className="flex flex-col justify-center "> <div className="flex flex-col justify-end">
<div className="text-3xl font-bold"> <div className="text-3xl font-bold">
{voucher.display_quota} {voucher.display_quota}
</div> </div>
<div className="text-gray-400 text-right"> <div className="text-gray-400 ">
{voucher.display_expired} {voucher.display_expired}
</div> </div>
</div> </div>

@ -0,0 +1,39 @@
import React from 'react'
import { Head, router } from '@inertiajs/react'
import CustomerLayout from '@/Layouts/CustomerLayout'
import VoucherCard from './VoucherCard'
import { HiChevronLeft } from 'react-icons/hi2'
export default function Detail({ sale }) {
return (
<CustomerLayout>
<Head title="Detail" />
<div className="flex flex-col min-h-[calc(95dvh)]">
<div
className="w-full px-5 py-5"
onClick={() => {
router.get(route('transactions.index'))
}}
>
<HiChevronLeft className="font-bold h-5 w-5" />
</div>
<div className="text-2xl px-5 font-bold">
Transaksi #{sale.code}
</div>
<div className="px-5 pb-4">{sale.format_created_at}</div>
<div className="w-full px-5 flex flex-col space-y-2">
{sale.items.map((item) => (
<VoucherCard key={item.id} item={item} />
))}
</div>
<div className="fixed bottom-20 right-0 w-full">
<div className="max-w-sm mx-auto text-xl font-bold text-right flex flex-row justify-between">
<div>TOTAL</div>
<div> {sale.display_amount}</div>
</div>
</div>
</div>
</CustomerLayout>
)
}

@ -0,0 +1,71 @@
import React, { useState } from 'react'
import { Head, router } from '@inertiajs/react'
import CustomerLayout from '@/Layouts/CustomerLayout'
export default function Index({ query: { data, next_page_url } }) {
const [sales, setSales] = useState(data)
const handleNextPage = () => {
router.get(
next_page_url,
{},
{
replace: true,
preserveState: true,
only: ['query'],
onSuccess: (res) => {
setSales(sales.concat(res.props.query.data))
},
}
)
}
return (
<CustomerLayout>
<Head title="Transaksi" />
<div className="flex flex-col min-h-[calc(95dvh)]">
<div className="py-5 text-2xl px-5 font-bold">
Transaksi Pembelian
</div>
<div className="w-full">
<div className="flex flex-col space-y-5 px-5">
{sales.map((sale) => (
<div
key={sale.id}
className="flex flex-row pb-2 items-center justify-between border-b"
onClick={() =>
router.get(
route('transactions.show', sale.id)
)
}
>
<div className="flex flex-col">
<div>{sale.format_human_created_at}</div>
<div className="font-thin">
Invoice{' '}
<span className="font-bold">
#{sale.code}
</span>
</div>
</div>
<div className="flex flex-col items-end">
<div className="font-bold text-lg">
{sale.display_amount}
</div>
</div>
</div>
))}
{next_page_url !== null && (
<div
onClick={handleNextPage}
className="w-full text-center px-2 py-1 border mt-5 hover:bg-blue-600 hover:text-white"
>
Load more
</div>
)}
</div>
</div>
</div>
</CustomerLayout>
)
}

@ -0,0 +1,73 @@
import { formatIDR } from '@/utils'
import { HiShare } from 'react-icons/hi2'
export default function VoucherCard({ item: { voucher, quantity } }) {
return (
<div className="px-3 py-1 shadow-md rounded border border-gray-100">
<div className="w-full flex flex-row justify-between py-0.5">
<div className="text-base font-bold">
{voucher.location.name}
</div>
<div
className="text-right"
onClick={() => {
navigator.share({
title: 'Hello World',
text: 'Hai Hai',
})
}}
>
<HiShare className="w-6 h-6" />
</div>
</div>
<div className="w-full border border-dashed"></div>
<div className="flex flex-row justify-between items-center">
<div>
<div className="text-xs text-gray-400 py-1">
{voucher.profile}
</div>
<div className="text-xl font-bold">
IDR {formatIDR(voucher.price)}
</div>
{+voucher.discount !== 0 && (
<div className="flex flex-row space-x-2 items-center text-xs pb-2">
<div className="bg-red-300 text-red-600 px-1 py-0.5 font-bold rounded">
{voucher.discount}%
</div>
<div className="text-gray-400 line-through">
{formatIDR(voucher.display_price)}
</div>
</div>
)}
</div>
<div className="flex flex-col justify-end">
<div className="text-3xl font-bold">
{voucher.display_quota}
</div>
<div className="text-gray-400">
{voucher.display_expired}
</div>
</div>
</div>
<div className="w-full border border-dashed"></div>
<div className="w-full flex flex-row justify-between items-center pt-1">
<div>{formatIDR(voucher.price)}</div>
<div>x</div>
<div>{quantity}</div>
<div>{formatIDR(+voucher.price * +quantity)}</div>
</div>
<div className="w-full flex flex-row justify-between items-center py-1">
<div className="w-full py-1 px-2 bg-blue-50 border border-blue-200 rounded text-blue-700">
<div>
Username :{' '}
<span className="font-bold">{voucher.username}</span>
</div>
<div>
Password :{' '}
<span className="font-bold">{voucher.password}</span>
</div>
</div>
</div>
</div>
)
}

@ -1,7 +1,7 @@
<?php <?php
use App\Http\Controllers\Api\RoleController;
use App\Http\Controllers\Api\LocationController; use App\Http\Controllers\Api\LocationController;
use App\Http\Controllers\Api\RoleController;
use App\Http\Controllers\Customer\DepositController; use App\Http\Controllers\Customer\DepositController;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;

@ -1,9 +1,11 @@
<?php <?php
use App\Http\Controllers\Customer\AuthController; use App\Http\Controllers\Customer\AuthController;
use App\Http\Controllers\Customer\CartController;
use App\Http\Controllers\Customer\DepositController; use App\Http\Controllers\Customer\DepositController;
use App\Http\Controllers\Customer\HomeController; use App\Http\Controllers\Customer\HomeController;
use App\Http\Controllers\Customer\ProfileController; use App\Http\Controllers\Customer\ProfileController;
use App\Http\Controllers\Customer\TransactionController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
/* /*
@ -36,8 +38,16 @@ Route::middleware(['http_secure_aware', 'guard_should_customer', 'inertia.custom
Route::post('deposit/topup', [DepositController::class, 'store']); Route::post('deposit/topup', [DepositController::class, 'store']);
Route::get('deposit/trx/{deposit}', [DepositController::class, 'show'])->name('customer.deposit.show'); Route::get('deposit/trx/{deposit}', [DepositController::class, 'show'])->name('customer.deposit.show');
Route::post('deposit/trx/{deposit}', [DepositController::class, 'update'])->name('customer.deposit.update'); Route::post('deposit/trx/{deposit}', [DepositController::class, 'update'])->name('customer.deposit.update');
});
// cart
Route::get('cart', [CartController::class, 'index'])->name('cart.index');
Route::post('cart/process', [CartController::class, 'purchase'])->name('cart.purchase');
Route::post('cart/{voucher}', [CartController::class, 'store'])->name('cart.store');
// transaction
Route::get('sale/trx', [TransactionController::class, 'index'])->name('transactions.index');
Route::get('sale/trx/{sale}', [TransactionController::class, 'show'])->name('transactions.show');
});
Route::middleware('guest:customer')->group(function () { Route::middleware('guest:customer')->group(function () {
// login // login

Loading…
Cancel
Save