coin price and coin exchange done

dev
Aji Kamaludin 1 year ago
parent 74b8848e15
commit e44b9e74a6
No known key found for this signature in database
GPG Key ID: 19058F67F0083AD3

@ -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

@ -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']);
}
/**

@ -0,0 +1,91 @@
<?php
namespace App\Http\Controllers\Customer;
use App\Http\Controllers\Controller;
use App\Models\Customer;
use App\Models\Location;
use App\Models\Sale;
use App\Models\Voucher;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class CoinExchangeController extends Controller
{
public function index(Request $request)
{
$locations = Location::get();
$vouchers = Voucher::with(['location'])
->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']);
}
}

@ -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()
]);
}
}

@ -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();

@ -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'],

@ -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, ',', '.');
});
}

@ -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, ',', '.');
});
}

@ -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, ',', '.');
});
}

@ -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, ',', '.');
});
}
}

@ -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,

@ -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.

@ -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'],

@ -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 (
<div className="w-full px-5 text-center flex flex-col my-auto">
<div className="font-bold text-xl">Voucher segera tersedia</div>
<div className="text-gray-400">
Yuk, share referral kamu untuk tingkatkan coinnya
</div>
</div>
)
}
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 (
<CustomerLayout>
<Head title="Coin" />
<div className="flex flex-col min-h-[calc(95dvh)]">
<div className="pt-5 text-2xl px-5 font-bold">Tukar Coin</div>
<div className="px-5 text-gray-400 text-sm">
tukarkan coin anda dengan voucher manarik
</div>
{v.length <= 0 ? (
<EmptyHere />
) : (
<div className="w-full flex flex-col pt-5">
{/* chips */}
<div className="w-full flex flex-row overflow-y-scroll space-x-2 px-2">
{locations.map((location) => (
<div
onClick={() => 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}
</div>
))}
</div>
{/* voucher */}
<div className="flex flex-col w-full px-3 mt-3 space-y-2">
{v.map((voucher) => (
<VoucherCard
key={voucher.id}
voucher={voucher}
/>
))}
{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,99 @@
import { formatIDR } from '@/utils'
import { router } from '@inertiajs/react'
import { useState } from 'react'
const ExchangeModal = ({ show, voucher, setShow }) => {
return (
<div
className={`fixed z-10 top-0 left-0 h-full w-full -mt-4 ${
show ? '' : 'invisible'
} `}
>
<div
className="max-w-md mx-auto h-full bg-gray-500 bg-opacity-70 -mt-2"
onClick={() => setShow(false)}
>
<div className="flex flex-col h-full my-auto justify-center px-4">
<div className="px-3 py-1 shadow-md rounded border bg-white border-gray-100 hover:bg-gray-50">
<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">
{formatIDR(voucher.price_coin)} Coin
</div>
</div>
<div className="flex flex-col justify-end text-right">
<div className="text-3xl font-bold">
{voucher.display_quota}
</div>
<div className="text-gray-400 ">
{voucher.display_expired}
</div>
</div>
</div>
</div>
<div className="flex flex-row space-x-3">
<div
className="w-full mt-2 px-3 py-1 shadow-md rounded border-blue-700 bg-blue-600 text-white hover:bg-white hover:text-black"
onClick={() =>
router.get(
route(
'customer.coin.exchange.process',
voucher
)
)
}
>
Tukarkan
</div>
<div className="w-full mt-2 px-3 py-1 shadow-md rounded border-white bg-white hover:bg-gray-200">
Batal
</div>
</div>
</div>
</div>
</div>
)
}
export default function VoucherCard({ voucher }) {
const [show, setShow] = useState(false)
return (
<>
<div
className="px-3 py-1 shadow-md rounded border border-gray-100 hover:bg-gray-50"
onClick={() => setShow(true)}
>
<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">
{formatIDR(voucher.price_coin)} Coin
</div>
</div>
<div className="flex flex-col justify-end text-right">
<div className="text-3xl font-bold">
{voucher.display_quota}
</div>
<div className="text-gray-400 ">
{voucher.display_expired}
</div>
</div>
</div>
</div>
<ExchangeModal voucher={voucher} show={show} setShow={setShow} />
</>
)
}

@ -23,7 +23,12 @@ export default function Detail({ sale }) {
<div className="px-5">{sale.format_created_at}</div>
<div className="px-5 pb-4 w-full">
<div className="text-xl font-bold text-right flex flex-row justify-between">
<div className="flex flex-col items-start">
<div>TOTAL</div>
<div className="text-xs font-thin text-gray-400">
pembayaran: {sale.payed_with}
</div>
</div>
<div> {sale.display_amount}</div>
</div>
</div>

@ -71,9 +71,9 @@ export default function CustomerLayout({ children }) {
</div>
<div
className={`pb-1 pt-2 px-5 hover:bg-blue-200 flex flex-col items-center ${isActive(
'coin'
'customer.coin.exchange'
)}`}
// onClick={() => handleOnClick('cart.index')}
onClick={() => handleOnClick('customer.coin.exchange')}
>
<div className="flex flex-row">
<HiOutlineGift className="h-6 w-6" />

@ -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}
/>
<FormInput
type="number"
name="price_coin"
value={data.price_coin}
onChange={handleOnChange}
label="Harga Coin (untuk penukaran)"
error={errors.price_coin}
/>
<FormInput
type="number"
name="quota"

@ -17,6 +17,7 @@ export default function Import(props) {
script: '',
discount: 0,
display_price: 0,
price_coin: 0,
expired: '',
expired_unit: 'Hari',
location_id: null,
@ -109,6 +110,14 @@ export default function Import(props) {
max={100}
min={0}
/>
<FormInput
type="number"
name="price_coin"
value={data.price_coin}
onChange={handleOnChange}
label="Harga Coin (untuk penukaran)"
error={errors.price_coin}
/>
<div>
<label className="block text-sm font-medium text-gray-900 dark:text-white">
Masa Aktif

@ -1,64 +1,64 @@
import moment from "moment";
import { isEmpty } from "lodash";
import moment from 'moment'
import { isEmpty } from 'lodash'
export const formatDate = (date) => {
return moment(date).format("DD/MM/yyyy");
};
return moment(date).format('DD/MM/yyyy')
}
export const formatDateTime = (date) => {
return moment(date).format("DD/MM/yyyy HH:mm:ss");
};
return moment(date).format('DD/MM/yyyy HH:mm:ss')
}
export const dateToString = (date) => {
return moment(date).format("MM/DD/yyyy");
};
return moment(date).format('MM/DD/yyyy')
}
export const converToDate = (date) => {
if (isEmpty(date) == false) {
return new Date(date);
return new Date(date)
}
return "";
};
return ''
}
export function formatIDR(amount) {
const idFormatter = new Intl.NumberFormat("id-ID",{
const idFormatter = new Intl.NumberFormat('id-ID', {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
});
return idFormatter.format(amount);
})
return idFormatter.format(amount)
}
export const formatIDDate = (date) => {
const month = [
"Januari",
"Februari",
"Maret",
"April",
"Mei",
"Juni",
"Juli",
"Agustus",
"September",
"Oktober",
"November",
"Desember",
];
date = new Date(date);
'Januari',
'Februari',
'Maret',
'April',
'Mei',
'Juni',
'Juli',
'Agustus',
'September',
'Oktober',
'November',
'Desember',
]
date = new Date(date)
return `${date.getDate()} ${month[date.getMonth()]} ${date.getFullYear()}`;
};
return `${date.getDate()} ${month[date.getMonth()]} ${date.getFullYear()}`
}
export const hasPermission = (auth, permission) => {
const { user } = auth
if (+user.is_superadmin === 1) {
return true;
return true
}
let has = user.role.permissions.find(item => item.name === permission)
let has = user.role.permissions.find((item) => item.name === permission)
if (has) {
return true
}
return false;
return false
}

@ -3,6 +3,7 @@
use App\Http\Controllers\Customer\AuthController;
use App\Http\Controllers\Customer\CartController;
use App\Http\Controllers\Customer\CoinController;
use App\Http\Controllers\Customer\CoinExchangeController;
use App\Http\Controllers\Customer\DepositController;
use App\Http\Controllers\Customer\HomeController;
use App\Http\Controllers\Customer\PaylaterController;
@ -52,9 +53,12 @@ Route::middleware(['http_secure_aware', 'guard_should_customer', 'inertia.custom
Route::post('deposit/trx/{deposit}', [DepositController::class, 'update'])->name('customer.deposit.update');
// coin
Route::get('coin/exchanges', [CoinExchangeController::class, 'index'])->name('customer.coin.exchange');
Route::get('coin/exchanges/{voucher}', [CoinExchangeController::class, 'exchange'])->name('customer.coin.exchange.process');
Route::get('coin', [CoinController::class, 'index'])->name('customer.coin.index');
Route::get('coin/{coin}', [CoinController::class, 'show'])->name('customer.coin.show');
// cart
Route::get('cart', [CartController::class, 'index'])->name('cart.index');
Route::post('cart/process', [CartController::class, 'purchase'])->name('cart.purchase');

Loading…
Cancel
Save