customer purchase done
parent
2f9cd2994c
commit
7e0887015b
@ -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']);
|
||||
}
|
||||
}
|
@ -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'])
|
||||
]);
|
||||
}
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue