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 Deposit Manual
- [x] Customer Deposit Payment Gateway
- [ ] Customer Purchase Voucher
- [ ] Register Refferal
- [x] Customer Purchase Voucher
- [ ] Customer Share Buyed Voucher, via WA dll
- [ ] Register Refferal
- [ ] Customer View Coin History
- [ ] Verified Akun
- [ ] Notification (purchase success, deposit success)

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

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

@ -4,8 +4,6 @@ namespace App\Http\Controllers\Customer;
use App\Http\Controllers\Controller;
use App\Models\Customer;
use App\Models\CustomerLevel;
use App\Models\CustomerLevelHistory;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
@ -72,7 +70,7 @@ class AuthController extends Controller
'fullname' => $user->name,
'name' => $user->nickname,
'email' => $user->email,
'username' => Str::slug($user->name . '_' . Str::random(5), '_'),
'username' => Str::slug($user->name.'_'.Str::random(5), '_'),
'google_id' => $user->id,
'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');
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',
'payment' => [
'required',
Rule::in([Setting::PAYMENT_MANUAL, Setting::PAYMENT_MIDTRANS])
]
Rule::in([Setting::PAYMENT_MANUAL, Setting::PAYMENT_MIDTRANS]),
],
]);
DB::beginTransaction();
$deposit = DepositHistory::make([
'customer_id' => auth()->id(),
'debit' => $request->amount,
'description' => 'Top Up #' . Str::random(5),
'description' => 'Top Up #'.Str::random(5),
'payment_channel' => $request->payment,
]);
@ -77,7 +77,7 @@ class DepositController extends Controller
'accounts' => Account::get(),
'midtrans_client_key' => Setting::getByKey('MIDTRANS_CLIENT_KEY'),
'is_production' => app()->isProduction(),
'direct' => $request->direct
'direct' => $request->direct,
]);
}
@ -93,10 +93,10 @@ class DepositController extends Controller
$deposit->update([
'image_prove' => $file->hashName('uploads'),
'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)

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

@ -26,7 +26,7 @@ class ProfileController extends Controller
'name' => 'string|required',
'address' => 'string|required',
'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',
'image' => 'nullable|image',
]);
@ -51,6 +51,6 @@ class ProfileController extends Controller
'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)
{
return inertia('Customer/Form', [
'customer' => $customer
'customer' => $customer,
]);
}
public function update(Request $request, Customer $customer)
{
$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',
'name' => 'required|string',
'fullname' => 'required|string',

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

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

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

@ -14,7 +14,7 @@ class SettingController extends Controller
return inertia('Setting/Index', [
'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', [
'query' => $query->paginate()
'query' => $query->paginate(),
]);
}

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

@ -29,6 +29,14 @@ class HandleInertiaCustomerRequests extends Middleware
*/
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), [
'app_name' => env('APP_NAME', 'App Name'),
'auth' => [
@ -38,7 +46,7 @@ class HandleInertiaCustomerRequests extends Middleware
'message' => fn () => $request->session()->get('message') ?? ['type' => null, 'message' => null],
],
'notification_count' => 0,
'carts' => []
'cart_count' => $cart_count,
]);
}
}

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

@ -43,7 +43,7 @@ class Customer extends Authenticatable
'image_url',
'display_deposit',
'display_coin',
'display_phone'
'display_phone',
];
protected static function booted(): void
@ -103,6 +103,7 @@ class Customer extends Authenticatable
if ($this->phone === null) {
return ' - ';
}
return '+62' . $this->phone;
});
}
@ -125,4 +126,14 @@ class Customer extends Authenticatable
{
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_created_at',
'amount',
'image_prove_url'
'image_prove_url',
];
public function status(): Attribute
@ -61,14 +61,14 @@ class DepositHistory extends Model
public function formatHumanCreatedAt(): Attribute
{
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
{
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) {
return 'Rp' . number_format($this->debit, 0, ',', '.');
}
return '-Rp' . number_format($this->credit, 0, ',', '.');
});
}
@ -102,6 +103,6 @@ class DepositHistory extends Model
public function update_customer_balance()
{
$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;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Carbon;
class Sale extends Model
{
const PAYED_WITH_MIDTRANS = 'midtrans';
@ -13,6 +16,7 @@ class Sale extends Model
const PAYED_WITH_COIN = 'coin';
protected $fillable = [
'code',
'customer_id',
'date_time',
'amount',
@ -23,4 +27,37 @@ class Sale extends Model
'payment_channel',
'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_id',
'price',
'quantity',
'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
{
const UNSOLD = 0;
const SOLD = 1;
protected $fillable = [
'name',
'description',
@ -21,8 +25,8 @@ class Voucher extends Model
'comment',
'expired',
'expired_unit',
'is_sold', //menandakan sudah terjual atau belum
// batch pada saat import , jadi ketika user ingin beli akan tetapi sudah sold ,
'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',
];
@ -73,4 +77,14 @@ class Voucher extends Model
{
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()
{
$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');
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;
}
public static function getCartEnablePayment()
{
// deposit
// coin
// paylater
}
}

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

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

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

@ -41,9 +41,9 @@ class DummySeeder extends Seeder
$images = ['1.webp', '2.webp', '3.webp'];
foreach ($images as $index => $image) {
Banner::create([
'title' => 'Banner ' . $index,
'image' => 'sample/' . $image,
'description' => '<h1>Banner </h1>'
'title' => 'Banner '.$index,
'image' => 'sample/'.$image,
'description' => '<h1>Banner </h1>',
]);
}
}
@ -52,7 +52,7 @@ class DummySeeder extends Seeder
{
$banks = [
['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) {
@ -72,7 +72,7 @@ class DummySeeder extends Seeder
foreach ($locations as $location) {
Location::create([
'name' => $location,
'description' => '-'
'description' => '-',
]);
}
}
@ -82,7 +82,6 @@ class DummySeeder extends Seeder
$vouchers = GeneralService::script_parser(file_get_contents(public_path('example.md')));
DB::beginTransaction();
foreach ([1, 2] as $loop) {
$batchId = Str::ulid();

@ -1,4 +1,4 @@
import React from 'react'
import React, { useEffect } from 'react'
import { ToastContainer, toast } from 'react-toastify'
import { router, usePage } from '@inertiajs/react'
@ -13,8 +13,11 @@ export default function CustomerLayout({ children }) {
const {
props: {
auth: { user },
cart_count,
flash,
},
} = usePage()
const handleOnClick = (r) => {
router.get(route(r))
}
@ -27,6 +30,12 @@ export default function CustomerLayout({ children }) {
return 'text-gray-600'
}
useEffect(() => {
if (flash.message !== null) {
toast(flash.message.message, { type: flash.message.type })
}
}, [flash])
return (
<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">
@ -42,18 +51,29 @@ export default function CustomerLayout({ children }) {
<HiOutlineHome className="h-6 w-6" />
<div className="text-xs font-light">Beranda</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">
<HiOutlineShoppingCart className="h-6 w-6" />
<div>
<div className="bg-blue-300 text-blue-600 rounded-lg px-1 text-xs -ml-2">
1
{cart_count}
</div>
</div>
</div>
<div className="text-xs font-light">Keranjang</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" />
<div className="text-xs font-light">Transaksi</div>
</div>

@ -22,7 +22,6 @@ export default function FormModal(props) {
},
payment_channel: '',
is_valid: 0,
status: '',
status_text: '',
text_color: '',
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 { router } from '@inertiajs/react'
export default function VoucherCard({ voucher }) {
const addCart = () => {
router.post(route('cart.store', voucher))
}
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="w-full border border-dashed"></div>
<div className="flex flex-row justify-between items-center">
@ -24,11 +31,11 @@ export default function VoucherCard({ voucher }) {
</div>
)}
</div>
<div className="flex flex-col justify-center ">
<div className="flex flex-col justify-end">
<div className="text-3xl font-bold">
{voucher.display_quota}
</div>
<div className="text-gray-400 text-right">
<div className="text-gray-400 ">
{voucher.display_expired}
</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
use App\Http\Controllers\Api\RoleController;
use App\Http\Controllers\Api\LocationController;
use App\Http\Controllers\Api\RoleController;
use App\Http\Controllers\Customer\DepositController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

@ -1,9 +1,11 @@
<?php
use App\Http\Controllers\Customer\AuthController;
use App\Http\Controllers\Customer\CartController;
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 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::get('deposit/trx/{deposit}', [DepositController::class, 'show'])->name('customer.deposit.show');
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 () {
// login

Loading…
Cancel
Save