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