From e44b9e74a6b14c74c6f582bddf2d290546b8bace Mon Sep 17 00:00:00 2001 From: Aji Kamaludin Date: Thu, 8 Jun 2023 03:10:25 +0700 Subject: [PATCH] coin price and coin exchange done --- TODO.md | 6 +- .../Controllers/Customer/CartController.php | 4 +- .../Customer/CoinExchangeController.php | 91 +++++++++++++ .../Controllers/Customer/HomeController.php | 2 +- app/Http/Controllers/GeneralController.php | 4 +- app/Http/Controllers/VoucherController.php | 6 + app/Models/CoinHistory.php | 4 +- app/Models/Customer.php | 4 +- app/Models/DepositHistory.php | 4 +- app/Models/PaylaterHistory.php | 4 +- app/Models/Sale.php | 16 ++- app/Models/SaleItem.php | 5 +- database/seeders/DummySeeder.php | 9 +- resources/js/Customer/Coin/Exchange.jsx | 121 ++++++++++++++++++ resources/js/Customer/Coin/VoucherCard.jsx | 99 ++++++++++++++ resources/js/Customer/Trx/Detail.jsx | 7 +- resources/js/Layouts/CustomerLayout.jsx | 4 +- resources/js/Pages/Voucher/Form.jsx | 10 ++ resources/js/Pages/Voucher/Import.jsx | 9 ++ resources/js/utils.js | 70 +++++----- routes/web.php | 4 + 21 files changed, 424 insertions(+), 59 deletions(-) create mode 100644 app/Http/Controllers/Customer/CoinExchangeController.php create mode 100644 resources/js/Customer/Coin/Exchange.jsx create mode 100644 resources/js/Customer/Coin/VoucherCard.jsx diff --git a/TODO.md b/TODO.md index 2a396ba..ae7fbbe 100644 --- a/TODO.md +++ b/TODO.md @@ -20,10 +20,10 @@ - [x] Dashboard (gafik hasil penjualan : disorting tanggal, lokasi dan customer) - [x] Notification ([x]manual deposit, [x]deposit success, [x]stock voucher, [x]sale) - [x] Voucher - harga per level , fixing detail transaksi -- [ ] Voucher - harga coin +- [x] Voucher - harga coin - [ ] View Customer Coin History -### Adds +### Add - hutang (paylater) adalah limit tiap customer jika deposit kurang dalam pembayaran voucher , setiap limit yang digunakan akan di potong / di lunasi ketika melakukan topup deposit - tukar coin adalah dengan menambahkan harga coin di voucher dan menambahkan 1 fitur di customer untuk explorer voucher yang memiliki harga coin, disimpan menjadi sale biasa dengan cara 1 kali penukaran adalah 1 voucher @@ -45,4 +45,4 @@ - [x] Verified Akun - [x] Paylater: index paylater, payment cart, deposite repay - [x] Notification ([x] purchase success, [x] deposit success) -- [ ] Coin Explorer: list voucher, modal voucher to excange +- [x] Coin Explorer: list voucher, modal voucher to excange diff --git a/app/Http/Controllers/Customer/CartController.php b/app/Http/Controllers/Customer/CartController.php index cb0bc1f..c82fadd 100644 --- a/app/Http/Controllers/Customer/CartController.php +++ b/app/Http/Controllers/Customer/CartController.php @@ -54,7 +54,7 @@ class CartController extends Controller 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']); + session()->flash('message', ['type' => 'success', 'message' => 'voucher ditambahkan ke keranjang']); } else { $carts = $carts->map(function ($item) use ($voucher, $operator) { if ($item['id'] == $voucher->id) { @@ -89,7 +89,7 @@ class CartController extends Controller session(['carts' => [ ['id' => $voucher->id, 'quantity' => 1, 'voucher' => $voucher], ]]); - session()->flash('message', ['type' => 'success', 'message' => 'voucher added to cart']); + session()->flash('message', ['type' => 'success', 'message' => 'voucher ditambahkan ke keranjang']); } /** diff --git a/app/Http/Controllers/Customer/CoinExchangeController.php b/app/Http/Controllers/Customer/CoinExchangeController.php new file mode 100644 index 0000000..90fb111 --- /dev/null +++ b/app/Http/Controllers/Customer/CoinExchangeController.php @@ -0,0 +1,91 @@ +where(function ($q) { + $q->where('price_coin', '!=', 0) + ->where('price_coin', '!=', null); + }) + ->where('is_sold', Voucher::UNSOLD) + ->groupBy('batch_id') + ->orderBy('updated_at', 'desc'); + + if ($request->location_id != '') { + $vouchers->where('location_id', $request->location_id); + } + + return inertia('Coin/Exchange', [ + 'locations' => $locations, + 'vouchers' => tap($vouchers->paginate(10))->setHidden(['username', 'password']), + '_location_id' => $request->location_id ?? '', + ]); + } + + public function exchange(Voucher $voucher) + { + $batchCount = $voucher->count_unsold(); + if ($batchCount < 1) { + return redirect()->route('customer.coin.exchange') + ->with('message', ['type' => 'error', 'message' => 'transaksi gagal, voucher sedang tidak tersedia']); + } + + $customer = Customer::find(auth()->id()); + + if ($customer->coin_balance < $voucher->price_coin) { + return redirect()->route('customer.coin.exchange') + ->with('message', ['type' => 'error', 'message' => 'koin kamu tidak cukup untuk ditukar voucher ini']); + } + + DB::beginTransaction(); + $sale = $customer->sales()->create([ + 'code' => 'Tukar Coin ' . str()->upper(str()->random(5)), + 'date_time' => now(), + 'amount' => 0, + 'payed_with' => Sale::PAYED_WITH_COIN, + ]); + + $voucher = $voucher->shuffle_unsold(); + $sale->items()->create([ + 'entity_type' => $voucher::class, + 'entity_id' => $voucher->id, + 'price' => $voucher->price_coin, + 'quantity' => 1, + 'additional_info_json' => json_encode([ + 'id' => $voucher->id, + 'quantity' => 1, + 'voucher' => $voucher->load(['location']) + ]), + ]); + + $voucher->update(['is_sold' => Voucher::SOLD]); + $voucher->check_stock_notification(); + + $sale->create_notification(); + + $coin = $customer->coins()->create([ + 'credit' => $voucher->price_coin, + 'description' => $sale->code, + ]); + + $coin->update_customer_balance(); + DB::commit(); + + return redirect()->route('transactions.show', $sale) + ->with('message', ['type' => 'success', 'message' => 'penukaran berhasil']); + } +} diff --git a/app/Http/Controllers/Customer/HomeController.php b/app/Http/Controllers/Customer/HomeController.php index dad4082..c852eed 100644 --- a/app/Http/Controllers/Customer/HomeController.php +++ b/app/Http/Controllers/Customer/HomeController.php @@ -47,7 +47,7 @@ class HomeController extends Controller Notification::where('entity_id', auth()->id())->where('is_read', Notification::UNREAD)->update(['is_read' => Notification::READ]); return inertia('Index/Notification', [ - 'notification' => Notification::where('entity_id', auth()->id())->orderBy('updated_at', 'desc')->paginate() + 'notification' => Notification::where('entity_id', auth()->id())->orderBy('created_at', 'desc')->paginate() ]); } } diff --git a/app/Http/Controllers/GeneralController.php b/app/Http/Controllers/GeneralController.php index d0aafcb..297e994 100644 --- a/app/Http/Controllers/GeneralController.php +++ b/app/Http/Controllers/GeneralController.php @@ -37,7 +37,7 @@ class GeneralController extends Controller ->where('is_valid', DepositHistory::STATUS_VALID) ->where('debit', '!=', '0') ->groupBy('customer_id') - ->orderBy('updated_at', 'desc') + ->orderBy('total', 'desc') ->with('customer') ->limit(20) ->selectRaw('sum(debit) as total, is_valid, customer_id') @@ -47,7 +47,7 @@ class GeneralController extends Controller ->join('sales', 'sales.id', '=', 'sale_items.sale_id') ->join('customers', 'customers.id', '=', 'sales.customer_id') ->groupBy('sales.customer_id') - ->orderBy('sale_items.updated_at', 'desc') + ->orderBy('total', 'desc') ->limit(20) ->selectRaw('sum(sale_items.price) as total, sum(quantity) as count, sales.customer_id, customers.name, entity_id') ->get(); diff --git a/app/Http/Controllers/VoucherController.php b/app/Http/Controllers/VoucherController.php index f116889..c896f4a 100644 --- a/app/Http/Controllers/VoucherController.php +++ b/app/Http/Controllers/VoucherController.php @@ -47,6 +47,7 @@ class VoucherController extends Controller 'password' => 'required|string', 'discount' => 'required|numeric', 'display_price' => 'required|numeric', + 'price_coin' => 'nullable|numeric', 'quota' => 'required|string', 'profile' => 'required|string', 'comment' => 'required|string', @@ -66,6 +67,7 @@ class VoucherController extends Controller 'password' => $request->password, 'discount' => $request->discount, 'display_price' => $request->display_price, + 'price_coin' => $request->price_coin, 'quota' => $request->quota, 'profile' => $request->profile, 'comment' => $request->comment, @@ -108,6 +110,7 @@ class VoucherController extends Controller 'password' => 'required|string', 'discount' => 'required|numeric', 'display_price' => 'required|numeric', + 'price_coin' => 'nullable|numeric', 'quota' => 'required|string', 'profile' => 'required|string', 'comment' => 'required|string', @@ -127,6 +130,7 @@ class VoucherController extends Controller 'password' => $request->password, 'discount' => $request->discount, 'display_price' => $request->display_price, + 'price_coin' => $request->price_coin, 'quota' => $request->quota, 'profile' => $request->profile, 'comment' => $request->comment, @@ -173,6 +177,7 @@ class VoucherController extends Controller 'location_id' => 'required|exists:locations,id', 'discount' => 'required|numeric', 'display_price' => 'required|numeric', + 'price_coin' => 'nullable|numeric', 'expired' => 'required|numeric', 'expired_unit' => 'required|string', 'prices' => 'nullable|array', @@ -191,6 +196,7 @@ class VoucherController extends Controller 'password' => $voucher['password'], 'discount' => $request->discount, 'display_price' => $request->display_price, + 'price_coin' => $request->price_coin, 'quota' => $voucher['quota'], 'profile' => $voucher['profile'], 'comment' => $voucher['comment'], diff --git a/app/Models/CoinHistory.php b/app/Models/CoinHistory.php index 265186b..8ba3a9f 100644 --- a/app/Models/CoinHistory.php +++ b/app/Models/CoinHistory.php @@ -41,10 +41,10 @@ class CoinHistory extends Model { return Attribute::make(get: function () { if ($this->credit == 0) { - return number_format($this->debit, 0, ',', '.'); + return number_format($this->debit, is_float($this->debit) ? 2 : 0, ',', '.'); } - return number_format($this->credit, 0, ',', '.'); + return number_format($this->credit, is_float($this->credit) ? 2 : 0, ',', '.'); }); } diff --git a/app/Models/Customer.php b/app/Models/Customer.php index d72a1d4..75ca6a2 100644 --- a/app/Models/Customer.php +++ b/app/Models/Customer.php @@ -133,14 +133,14 @@ class Customer extends Authenticatable public function displayDeposit(): Attribute { return Attribute::make(get: function () { - return number_format($this->deposit_balance, 0, ',', '.'); + return number_format($this->deposit_balance, is_float($this->deposit_balance) ? 2 : 0, ',', '.'); }); } public function displayCoin(): Attribute { return Attribute::make(get: function () { - return number_format($this->coin_balance, 0, ',', '.'); + return number_format($this->coin_balance, is_float($this->coin_balance) ? 2 : 0, ',', '.'); }); } diff --git a/app/Models/DepositHistory.php b/app/Models/DepositHistory.php index 8d12f99..65d8efa 100644 --- a/app/Models/DepositHistory.php +++ b/app/Models/DepositHistory.php @@ -76,10 +76,10 @@ class DepositHistory extends Model { return Attribute::make(get: function () { if ($this->credit == 0) { - return 'Rp' . number_format($this->debit, 0, ',', '.'); + return 'Rp' . number_format($this->debit, is_float($this->debit) ? 2 : 0, ',', '.'); } - return '-Rp' . number_format($this->credit, 0, ',', '.'); + return '-Rp' . number_format($this->credit, is_float($this->credit) ? 2 : 0, ',', '.'); }); } diff --git a/app/Models/PaylaterHistory.php b/app/Models/PaylaterHistory.php index 5579648..b38a021 100644 --- a/app/Models/PaylaterHistory.php +++ b/app/Models/PaylaterHistory.php @@ -45,10 +45,10 @@ class PaylaterHistory extends Model { return Attribute::make(get: function () { if ($this->credit == 0) { - return 'Rp' . number_format($this->debit, 0, ',', '.'); + return 'Rp' . number_format($this->debit, is_float($this->debit) ? 2 : 0, ',', '.'); } - return '-Rp' . number_format($this->credit, 0, ',', '.'); + return '-Rp' . number_format($this->credit, is_float($this->credit) ? 2 : 0, ',', '.'); }); } } diff --git a/app/Models/Sale.php b/app/Models/Sale.php index 2b9d35c..bec0d5e 100644 --- a/app/Models/Sale.php +++ b/app/Models/Sale.php @@ -64,12 +64,26 @@ class Sale extends Model public function displayAmount(): Attribute { return Attribute::make(get: function () { - return 'Rp' . number_format($this->amount, 0, ',', '.'); + return 'Rp' . number_format($this->amount, is_float($this->amount) ? 2 : 0, ',', '.'); }); } public function create_notification() { + if ($this->payed_with == self::PAYED_WITH_COIN) { + Notification::create([ + 'entity_type' => User::class, + 'description' => $this->customer->fullname . ' melakukan penukaran ' . $this->items()->count() . ' voucher sebesar ' . $this->items->value('price') . ' coin', + ]); + + Notification::create([ + 'entity_id' => auth()->id(), + 'description' => 'Transaksi ' . $this->code . ' berhasil', + ]); + + return; + } + Notification::create([ 'entity_type' => User::class, 'description' => $this->customer->fullname . ' melakukan pembelian ' . $this->items()->count() . ' voucher sebesar ' . $this->display_amount, diff --git a/app/Models/SaleItem.php b/app/Models/SaleItem.php index c339f83..97e7a04 100644 --- a/app/Models/SaleItem.php +++ b/app/Models/SaleItem.php @@ -38,6 +38,9 @@ class SaleItem extends Model { return Attribute::make(get: function () { $item = json_decode($this->additional_info_json); + if ($item == null) { + return ''; + } $string = "Hai, aku baru beli voucher {$item->voucher->location->name} di " . route('home.index'); $string .= " voucher {$item->voucher->display_quota} buat {$item->voucher->display_expired} @@ -45,7 +48,7 @@ Username : {$item->voucher->username} Password : {$item->voucher->password} "; - $string .= "Cuman Rp" . number_format($this->price, '0', ',', '.') . " aja, "; + $string .= "Cuman Rp" . number_format($this->price, is_float($this->price) ? 2 : 0, ',', '.') . " aja, "; if ($item->voucher->discount > 0) { $string .= "lagi ada discount {$item->voucher->discount}% loh. diff --git a/database/seeders/DummySeeder.php b/database/seeders/DummySeeder.php index 6696939..061defd 100644 --- a/database/seeders/DummySeeder.php +++ b/database/seeders/DummySeeder.php @@ -41,8 +41,8 @@ class DummySeeder extends Seeder $images = ['1.webp', '2.webp', '3.webp']; foreach ($images as $index => $image) { Banner::create([ - 'title' => 'Banner '.$index, - 'image' => 'sample/'.$image, + 'title' => 'Banner ' . $index, + 'image' => 'sample/' . $image, 'description' => '

Banner

', ]); } @@ -83,10 +83,12 @@ class DummySeeder extends Seeder $vouchers = GeneralService::script_parser(file_get_contents(public_path('example.md'))); DB::beginTransaction(); - foreach ([1, 2] as $loop) { + foreach ([1, 2, 3] as $loop) { $batchId = Str::ulid(); $location = Location::get()[$loop]; + $price_coin = $loop == 3 ? 10 : 0; + foreach ($vouchers as $voucher) { Voucher::create([ 'location_id' => $location->id, @@ -94,6 +96,7 @@ class DummySeeder extends Seeder 'password' => $voucher['password'], 'discount' => $loop == 1 ? 10 : 0, 'display_price' => $loop == 1 ? 100000 : 99000, + 'price_coin' => $price_coin, 'quota' => $voucher['quota'], 'profile' => $voucher['profile'], 'comment' => $voucher['comment'], diff --git a/resources/js/Customer/Coin/Exchange.jsx b/resources/js/Customer/Coin/Exchange.jsx new file mode 100644 index 0000000..3e8b4cf --- /dev/null +++ b/resources/js/Customer/Coin/Exchange.jsx @@ -0,0 +1,121 @@ +import React, { useState } from 'react' +import { Head, router } from '@inertiajs/react' + +import CustomerLayout from '@/Layouts/CustomerLayout' +import VoucherCard from './VoucherCard' + +const EmptyHere = () => { + return ( +
+
Voucher segera tersedia
+
+ Yuk, share referral kamu untuk tingkatkan coinnya +
+
+ ) +} + +export default function Exhange(props) { + const { + locations, + vouchers: { data, next_page_url }, + _location_id, + } = props + + const [locId, setLocId] = useState(_location_id) + const [v, setV] = useState(data) + + const handleSelectLoc = (loc) => { + if (loc.id === locId) { + setLocId('') + fetch('') + return + } + setLocId(loc.id) + fetch(loc.id) + } + + const handleNextPage = () => { + router.get( + next_page_url, + { + location_id: locId, + }, + { + replace: true, + preserveState: true, + only: ['vouchers'], + onSuccess: (res) => { + setV(v.concat(res.props.vouchers.data)) + }, + } + ) + } + + const fetch = (locId) => { + router.get( + route(route().current()), + { location_id: locId }, + { + replace: true, + preserveState: true, + onSuccess: (res) => { + setV(res.props.vouchers.data) + }, + } + ) + } + + return ( + + +
+
Tukar Coin
+
+ tukarkan coin anda dengan voucher manarik +
+ + {v.length <= 0 ? ( + + ) : ( +
+ {/* chips */} +
+ {locations.map((location) => ( +
handleSelectLoc(location)} + key={location.id} + className={`px-2 py-1 rounded-2xl ${ + location.id === locId + ? 'text-white bg-blue-600 border border-blue-800' + : 'bg-blue-100 border border-blue-200' + }`} + > + {location.name} +
+ ))} +
+ + {/* voucher */} +
+ {v.map((voucher) => ( + + ))} + {next_page_url !== null && ( +
+ Load more +
+ )} +
+
+ )} +
+
+ ) +} diff --git a/resources/js/Customer/Coin/VoucherCard.jsx b/resources/js/Customer/Coin/VoucherCard.jsx new file mode 100644 index 0000000..49f9fa5 --- /dev/null +++ b/resources/js/Customer/Coin/VoucherCard.jsx @@ -0,0 +1,99 @@ +import { formatIDR } from '@/utils' +import { router } from '@inertiajs/react' +import { useState } from 'react' + +const ExchangeModal = ({ show, voucher, setShow }) => { + return ( +
+
setShow(false)} + > +
+
+
+ {voucher.location.name} +
+
+
+
+
+ {voucher.profile} +
+
+ {formatIDR(voucher.price_coin)} Coin +
+
+
+
+ {voucher.display_quota} +
+
+ {voucher.display_expired} +
+
+
+
+
+
+ router.get( + route( + 'customer.coin.exchange.process', + voucher + ) + ) + } + > + Tukarkan +
+
+ Batal +
+
+
+
+
+ ) +} + +export default function VoucherCard({ voucher }) { + const [show, setShow] = useState(false) + return ( + <> +
setShow(true)} + > +
+ {voucher.location.name} +
+
+
+
+
+ {voucher.profile} +
+
+ {formatIDR(voucher.price_coin)} Coin +
+
+
+
+ {voucher.display_quota} +
+
+ {voucher.display_expired} +
+
+
+
+ + + ) +} diff --git a/resources/js/Customer/Trx/Detail.jsx b/resources/js/Customer/Trx/Detail.jsx index e0a2e09..80eb690 100644 --- a/resources/js/Customer/Trx/Detail.jsx +++ b/resources/js/Customer/Trx/Detail.jsx @@ -23,7 +23,12 @@ export default function Detail({ sale }) {
{sale.format_created_at}
-
TOTAL
+
+
TOTAL
+
+ pembayaran: {sale.payed_with} +
+
{sale.display_amount}
diff --git a/resources/js/Layouts/CustomerLayout.jsx b/resources/js/Layouts/CustomerLayout.jsx index 8663a59..e81c5b1 100644 --- a/resources/js/Layouts/CustomerLayout.jsx +++ b/resources/js/Layouts/CustomerLayout.jsx @@ -71,9 +71,9 @@ export default function CustomerLayout({ children }) {
handleOnClick('cart.index')} + onClick={() => handleOnClick('customer.coin.exchange')} >
diff --git a/resources/js/Pages/Voucher/Form.jsx b/resources/js/Pages/Voucher/Form.jsx index 56a320b..75143c5 100644 --- a/resources/js/Pages/Voucher/Form.jsx +++ b/resources/js/Pages/Voucher/Form.jsx @@ -18,6 +18,7 @@ export default function Form(props) { password: '', discount: 0, display_price: 0, + price_coin: 0, quota: '', profile: '', comment: '', @@ -95,6 +96,7 @@ export default function Form(props) { password: voucher.password, discount: voucher.discount, display_price: voucher.display_price, + price_coin: voucher.price_coin, quota: voucher.quota, profile: voucher.profile, comment: voucher.comment, @@ -161,6 +163,14 @@ export default function Form(props) { max={100} min={0} /> + +