penukaran poin

dev
Aji Kamaludin 1 year ago
parent 8059a523dd
commit 231a4511fd
No known key found for this signature in database
GPG Key ID: 19058F67F0083AD3

@ -5,8 +5,10 @@ namespace App\Http\Controllers\Customer;
use App\Http\Controllers\Controller;
use App\Models\Customer;
use App\Models\Location;
use App\Models\LocationProfile;
use App\Models\Sale;
use App\Models\Voucher;
use App\Services\GeneralService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@ -14,53 +16,55 @@ class PoinExchangeController extends Controller
{
public function index(Request $request)
{
$favorite = $request->favorite ?? 1;
$customer = $request->user('customer');
$flocations = $customer->locationFavorites;
$slocations = [];
$favorite = $request->favorite ?? 0;
if ($request->favorite == '') {
$favorite = $customer->locationFavorites->count() > 0 ? 1 : 0;
}
$locations = Location::orderBy('name', 'asc')->get();
$vouchers = Voucher::with(['locationProfile.location'])
->whereHas('locationProfile', function ($q) {
$q->where('price_poin', '!=', 0)
->orWhereHas('prices', function ($q) {
$profiles = LocationProfile::with(['location'])
->whereHas('vouchers', function ($q) {
$q->where('is_sold', Voucher::UNSOLD);
})
->where(function ($q) {
$q->where('price_poin', '!=', 0)->orWhereHas('prices', function ($q) {
return $q->where('price_poin', '!=', 0);
});
})
->where('is_sold', Voucher::UNSOLD)
->groupBy('location_profile_id')
->orderBy('updated_at', 'desc');
if ($favorite == 0) {
if ($request->location_ids != '') {
$vouchers->whereHas('locationProfile', function ($q) use ($request) {
return $q->whereIn('location_id', $request->location_ids);
});
$profiles->whereIn('location_id', $request->location_ids);
$profiles = $profiles->paginate(20);
$slocations = Location::whereIn('id', $request->location_ids)->get();
$vouchers = tap($vouchers->paginate(20))->setHidden(['username', 'password']);
}
}
if ($request->location_ids == '' && $flocations->count() > 0) {
$favorite = 1;
$vouchers->whereHas('locationProfile', function ($q) use ($flocations) {
return $q->whereIn('location_id', $flocations->pluck('id')->toArray());
});
$vouchers = tap($vouchers->paginate(20))->setHidden(['username', 'password']);
if ($favorite == 1) {
$profiles->whereIn('location_id', $flocations->pluck('id')->toArray());
$profiles = $profiles->paginate(20);
}
return inertia('Poin/Exchange', [
return inertia('PoinExchange/Index', [
'locations' => $locations,
'vouchers' => $vouchers,
'profiles' => $profiles,
'_slocations' => $slocations,
'_flocations' => $flocations,
'_favorite' => $favorite
]);
}
public function exchange(Voucher $voucher)
public function exchange(LocationProfile $profile)
{
$batchCount = $voucher->count_unsold();
$batchCount = $profile->count_unsold();
if ($batchCount < 1) {
return redirect()->route('customer.poin.exchange')
->with('message', ['type' => 'error', 'message' => 'transaksi gagal, voucher sedang tidak tersedia']);
@ -68,43 +72,45 @@ class PoinExchangeController extends Controller
$customer = Customer::find(auth()->id());
if ($customer->poin_balance < $voucher->price_poin) {
if ($customer->poin_balance < $profile->validate_price_poin) {
return redirect()->route('customer.poin.exchange')
->with('message', ['type' => 'error', 'message' => 'koin kamu tidak cukup untuk ditukar voucher ini']);
}
DB::beginTransaction();
$sale = $customer->sales()->create([
'code' => 'Tukar poin ' . str()->upper(str()->random(5)), //TODO changes this
'code' => GeneralService::generateExchangePoinCode(),
'date_time' => now(),
'amount' => 0,
'amount' => $profile->validate_price_poin,
'payed_with' => Sale::PAYED_WITH_POIN,
]);
$voucher = $voucher->shuffle_unsold(1);
$vouchers = $profile->shuffle_unsold(1);
foreach ($vouchers as $voucher) {
$sale->items()->create([
'entity_type' => $voucher::class,
'entity_id' => $voucher->id,
'price' => $voucher->price_poin,
'price' => $voucher->validate_price_poin,
'quantity' => 1,
'additional_info_json' => json_encode([
'id' => $voucher->id,
'quantity' => 1,
'voucher' => $voucher->load(['location']),
'voucher' => $voucher->load(['locationProfile.location'])
]),
]);
$voucher->update(['is_sold' => Voucher::SOLD]);
$voucher->check_stock_notification();
$sale->create_notification();
$poin = $customer->poins()->create([
'credit' => $voucher->price_poin,
'credit' => $voucher->validate_price_poin,
'description' => $sale->code,
'narration' => 'Penukaran Voucher Poin'
]);
$poin->update_customer_balance();
}
$sale->create_notification();
DB::commit();
return redirect()->route('transactions.sale.show', $sale)

@ -42,6 +42,8 @@ class LocationProfile extends Model
'validate_price',
'validate_display_price',
'validate_discount',
'validate_price_poin',
'validate_bonus_poin',
];
protected static function booted(): void
@ -158,6 +160,38 @@ class LocationProfile extends Model
});
}
public function validateBonusPoin(): Attribute
{
return Attribute::make(get: function () {
if ($this->prices->count() > 0) {
$price = $this->prices;
if (auth()->guard('customer')->check()) {
$customer = self::getInstance()['customer'];
return $price->where('customer_level_id', $customer->customer_level_id)
->value('bonus_poin');
}
return $price->max('bonus_poin');
}
return $this->bonus_poin;
});
}
public function validatePricePoin(): Attribute
{
return Attribute::make(get: function () {
if ($this->prices->count() > 0) {
$price = $this->prices;
if (auth()->guard('customer')->check()) {
$customer = self::getInstance()['customer'];
return $price->where('customer_level_id', $customer->customer_level_id)
->value('price_poin');
}
return $price->max('price_poin');
}
return $this->price_poin;
});
}
public function shuffle_unsold($limit)
{
$vouchers = Voucher::where([

@ -160,7 +160,9 @@ class GeneralService
public static function generateSaleVoucherCode()
{
$code = Sale::whereDate('created_at', now())->count() + 1;
$code = Sale::whereDate('created_at', '=', now())
->where('payed_with', '!=', Sale::PAYED_WITH_POIN)
->count() + 1;
return 'Invoice #VCR' . now()->format('dmy') . GeneralService::formatNumberCode($code);
}
@ -172,6 +174,15 @@ class GeneralService
return 'Invoice #BPN' . now()->format('dmy') . GeneralService::formatNumberCode($code);
}
public static function generateExchangePoinCode()
{
$code = Sale::whereDate('created_at', '=', now())
->where('payed_with', '=', Sale::PAYED_WITH_POIN)
->count() + 1;
return 'Invoice #PVC' . now()->format('dmy') . GeneralService::formatNumberCode($code);
}
public static function formatNumberCode($number)
{
if ($number < 10) {

@ -77,7 +77,7 @@ export default function Index(props) {
<HeaderTrx enable="poin" dates={dates} setDates={setDates} />
{_poins.length <= 0 && <EmptyHere />}
<div className="w-full">
<div className="flex flex-col py-10 space-y-5 px-5">
<div className="flex flex-col py-1 space-y-5 px-5">
{_poins.length > 0 && (
<div className="text-sm text-gray-400">
{formatIDDate(dates.startDate)} s/d{' '}

@ -1,5 +1,5 @@
import React, { useState } from 'react'
import { Head, router } from '@inertiajs/react'
import { Head, router, useForm } from '@inertiajs/react'
import { HiXMark, HiOutlineStar } from 'react-icons/hi2'
import CustomerLayout from '@/Layouts/CustomerLayout'
@ -9,6 +9,7 @@ import FormLocation from '@/Customer/Components/FormLocation'
import { ALL, FAVORITE } from '../Index/utils'
import { useModalState } from '@/hooks'
import { isEmpty } from 'lodash'
const EmptyHere = () => {
return (
@ -21,18 +22,19 @@ const EmptyHere = () => {
)
}
export default function Exhange(props) {
export default function Index(props) {
const {
locations,
vouchers: { data, next_page_url },
profiles: { data, next_page_url },
_favorite,
_slocations,
_flocations,
} = props
const [favorite, setFavorite] = useState(_favorite)
const [vouchers, setVouchers] = useState(data)
const { post, processing } = useForm({})
const [favorite, setFavorite] = useState(_favorite)
const [items, setItems] = useState(data ?? [])
const [fLocations] = useState(_flocations)
const [sLocations, setSLocations] = useState(_slocations)
const locationModal = useModalState()
@ -46,25 +48,32 @@ export default function Exhange(props) {
{
replace: true,
preserveState: true,
only: ['vouchers'],
only: ['profiles'],
onSuccess: (res) => {
setVouchers(vouchers.concat(res.props.vouchers.data))
setItems(items.concat(res.props.profiles.data))
},
}
)
}
const fetch = (locations) => {
let location_ids = locations.map((l) => l.id)
const fetch = (locations, favorite) => {
let location_ids = []
if (+favorite === +ALL) {
location_ids = locations.map((l) => l.id)
}
router.get(
route(route().current()),
{ location_ids },
{ location_ids, favorite },
{
replace: true,
preserveState: true,
onSuccess: (res) => {
setVouchers(res.props.vouchers.data)
if (isEmpty(res.props.profiles.data)) {
setItems([])
return
}
setItems(res.props.profiles.data)
},
}
)
@ -75,18 +84,29 @@ export default function Exhange(props) {
if (!isExists) {
const locations = [location].concat(...sLocations)
setSLocations(locations)
fetch(locations)
fetch(locations, ALL)
}
}
const handleRemoveLocation = (index) => {
const locations = sLocations.filter((_, i) => i !== index)
setSLocations(locations)
fetch(locations)
fetch(locations, favorite)
}
const handleFavoriteRemoveLocation = (location) => {
if (processing) {
return
}
post(route('customer.location.favorite', location), {
onSuccess: () => {
router.visit(route(route().current()))
},
})
}
const isStatus = (s) => {
if (s === favorite) {
if (+s === +favorite) {
return 'px-2 py-1 rounded-2xl hover:bg-blue-800 text-white bg-blue-600 border border-blue-800'
}
return 'px-2 py-1 rounded-2xl hover:bg-blue-800 hover:text-white bg-blue-100 border border-blue-200'
@ -94,12 +114,12 @@ export default function Exhange(props) {
const handleFavorite = () => {
setFavorite(FAVORITE)
fetch(fLocations)
fetch(fLocations, FAVORITE)
}
const handleAll = () => {
setFavorite(ALL)
fetch(sLocations)
fetch(sLocations, ALL)
}
return (
@ -110,6 +130,7 @@ export default function Exhange(props) {
<div className="px-5 text-gray-400 text-sm">
tukarkan poin anda dengan voucher manarik
</div>
<div className="w-full flex flex-col pt-5">
<div className="w-full flex flex-col">
<div className="w-full flex flex-row space-x-2 px-4">
@ -124,19 +145,23 @@ export default function Exhange(props) {
</div>
</div>
</div>
{favorite === ALL ? (
<>
<div
className="w-full space-x-2 px-4 my-2"
onClick={locationModal.toggle}
>
<FormLocation placeholder="Cari Lokasi" />
</div>
{favorite === ALL ? (
<div className="w-full flex flex-row overflow-y-scroll space-x-2 px-4">
{sLocations.map((location, index) => (
<div
className="flex flex-row items-center gap-1 px-2 py-1 rounded-2xl bg-blue-100 border border-blue-200 hover:bg-blue-500"
key={location.id}
onClick={() => handleRemoveLocation(index)}
onClick={() =>
handleRemoveLocation(index)
}
>
<div>{location.name}</div>
<div className="pl-2">
@ -145,13 +170,16 @@ export default function Exhange(props) {
</div>
))}
</div>
</>
) : (
<div className="w-full flex flex-row overflow-y-scroll space-x-2 px-4">
<div className="w-full flex flex-row overflow-y-scroll space-x-2 px-4 my-2">
{fLocations.map((location, index) => (
<div
className="flex flex-row items-center gap-1 px-2 py-1 rounded-2xl bg-blue-100 border border-blue-200 hover:bg-blue-500"
key={location.id}
onClick={() => handleRemoveLocation(index)}
onClick={() =>
handleFavoriteRemoveLocation(location)
}
>
<div>{location.name}</div>
<div className="pl-2">
@ -162,17 +190,15 @@ export default function Exhange(props) {
</div>
)}
</div>
{vouchers.length <= 0 ? (
{items.length <= 0 ? (
<EmptyHere />
) : (
<div className="w-full flex flex-col">
{/* voucher */}
<div className="flex flex-col w-full px-3 mt-3 space-y-2">
{vouchers.map((voucher) => (
<VoucherCard
key={voucher.id}
voucher={voucher}
/>
{items.map((item) => (
<VoucherCard key={item.id} item={item} />
))}
{next_page_url !== null && (
<div
@ -186,6 +212,7 @@ export default function Exhange(props) {
</div>
)}
</div>
<LocationModal
state={locationModal}
locations={locations}

@ -0,0 +1,3 @@
export function AllVoucher() {
return <div className="hai">All Voucher</div>
}

@ -0,0 +1,3 @@
export function FavoriteVoucher() {
return <div className="hai">All Voucher</div>
}

@ -3,41 +3,48 @@ import { router } from '@inertiajs/react'
import { useState } from 'react'
import BottomSheet from '../Components/BottomSheet'
const ExchangeModal = ({ show, voucher, setShow }) => {
const ExchangeModal = ({ show, item, setShow }) => {
return (
<BottomSheet isOpen={show} toggle={() => setShow(false)}>
<div className="flex flex-col h-full my-auto justify-center px-2 mt-2">
<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_profile.name}
{item.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.location_profile.display_note}
{item.display_note}
</div>
<div className="text-xl font-bold">
{formatIDR(voucher.location_profile.price_poin)}{' '}
poin{' '}
{formatIDR(item.validate_price_poin)} poin{' '}
</div>
</div>
<div className="flex flex-col justify-end text-right">
<div className="text-3xl font-bold">
{voucher.location_profile.quota}
{item.quota}
</div>
<div className="text-gray-400 ">
{voucher.location_profile.display_expired}
{item.display_expired}
</div>
</div>
</div>
</div>
{item.display_note !== null && (
<div
className="p-4 my-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-gray-800 dark:text-blue-400"
role="alert"
>
{item.display_note}
</div>
)}
<div className="flex flex-row space-x-3 mt-2">
<div
className="w-full text-center px-3 py-2 rounded-lg bg-blue-700 border border-blue-900 text-white hover:bg-blue-900"
onClick={() =>
router.get(
route('customer.poin.exchange.process', voucher)
route('customer.poin.exchange.process', item)
)
}
>
@ -55,7 +62,7 @@ const ExchangeModal = ({ show, voucher, setShow }) => {
)
}
export default function VoucherCard({ voucher }) {
export default function VoucherCard({ item }) {
const [show, setShow] = useState(false)
return (
<>
@ -63,30 +70,26 @@ export default function VoucherCard({ voucher }) {
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_profile.location.name}
</div>
<div className="text-base font-bold">{item.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.location_profile.display_note}
{item.display_note}
</div>
<div className="text-xl font-bold">
{formatIDR(voucher.validate_price_poin)} poin
{formatIDR(item.validate_price_poin)} poin
</div>
</div>
<div className="flex flex-col justify-end text-right">
<div className="text-3xl font-bold">
{voucher.location_profile.quota}
</div>
<div className="text-3xl font-bold">{item.quota}</div>
<div className="text-gray-400 ">
{voucher.location_profile.display_expired}
{item.display_expired}
</div>
</div>
</div>
</div>
<ExchangeModal voucher={voucher} show={show} setShow={setShow} />
<ExchangeModal item={item} show={show} setShow={setShow} />
</>
)
}

@ -3,6 +3,7 @@ import { Head, router } from '@inertiajs/react'
import CustomerLayout from '@/Layouts/CustomerLayout'
import VoucherCard from './VoucherCard'
import { HiChevronLeft } from 'react-icons/hi2'
import { convertPayedWith } from '../utils'
export default function Detail({ sale }) {
return (
@ -24,10 +25,7 @@ export default function Detail({ sale }) {
<div className="flex flex-col items-start">
<div>TOTAL</div>
<div className="text-xs font-thin text-gray-400">
pembayaran:{' '}
{sale.payed_with === 'paylater'
? 'saldo hutang'
: 'saldo deposit'}
{convertPayedWith(sale.payed_with)}
</div>
</div>
<div> {sale.display_amount}</div>

@ -1,3 +1,8 @@
import {
PAYED_WITH_DEPOSIT,
PAYED_WITH_PAYLATER,
PAYED_WITH_POIN,
} from '@/constant'
import toast from 'react-hot-toast'
export const toastSuccess = (message) => {
@ -6,4 +11,23 @@ export const toastSuccess = (message) => {
})
}
export const toastError = (message) => {
toast.error((t) => {
return <div onClick={() => toast.dismiss(t.id)}>{message}</div>
})
}
export const convertPayedWith = (payed_with) => {
const payedWith = [
{
key: PAYED_WITH_PAYLATER,
value: 'pembayaran: saldo hutang',
},
{ key: PAYED_WITH_POIN, value: 'penukaran poin' },
{ key: PAYED_WITH_DEPOSIT, value: 'pembayaran saldo deposit' },
]
return payedWith.find((p) => p.key === payed_with).value
}
export const CASH_DEPOSIT = 'CASH_DEPOSIT'

@ -1,6 +1,5 @@
import React, { useEffect, useState } from 'react'
import toast, { Toaster } from 'react-hot-toast'
import { Toaster } from 'react-hot-toast'
import { router, usePage } from '@inertiajs/react'
import { HiOutlineHome } from 'react-icons/hi'
import {
@ -10,6 +9,8 @@ import {
HiOutlineShoppingCart,
} from 'react-icons/hi2'
import { toastError, toastSuccess } from '@/Customer/utils'
export default function CustomerLayout({ children }) {
const {
props: {
@ -40,13 +41,11 @@ export default function CustomerLayout({ children }) {
useEffect(() => {
let se
if (flash.message !== null && flash.message.type !== null) {
toast.success((t) => {
return (
<div onClick={() => toast.dismiss(t.id)}>
{flash.message.message}
</div>
)
})
if (flash.message.type === 'error') {
toastError(flash.message.message)
return
}
toastSuccess(flash.message.message)
if (+flash.message.cart === 1) {
setBouce(true)
se = setTimeout(clearAnimate, 3000)

@ -45,8 +45,8 @@ export default function Detail(props) {
const { sale } = props
return (
<AuthenticatedLayout page={`Sale`} action={`Invoice #${sale.code}`}>
<Head title={`Invoice #${sale.code}`} />
<AuthenticatedLayout page={`Sale`} action={`${sale.code}`}>
<Head title={`${sale.code}`} />
<div>
<div className="mx-auto sm:px-6 lg:px-8">

@ -11,3 +11,13 @@ export const PAYMENT_MANUAL = 'MANUAL'
export const PAYMENT_MIDTRANS = 'MIDTRANS'
export const PAYMENT_CASH_DEPOSIT = 'CASH_DEPOSIT'
export const PAYED_WITH_MIDTRANS = 'midtrans'
export const PAYED_WITH_MANUAL = 'manual'
export const PAYED_WITH_DEPOSIT = 'deposit'
export const PAYED_WITH_PAYLATER = 'paylater'
export const PAYED_WITH_POIN = 'poin'

@ -68,7 +68,7 @@ Route::middleware(['http_secure_aware', 'guard_should_customer', 'inertia.custom
// poin exchange
Route::get('poin/exchanges', [PoinExchangeController::class, 'index'])->name('customer.poin.exchange');
Route::get('poin/exchanges/{voucher}', [PoinExchangeController::class, 'exchange'])->name('customer.poin.exchange.process');
Route::get('poin/exchanges/{profile}', [PoinExchangeController::class, 'exchange'])->name('customer.poin.exchange.process');
// cart
Route::get('cart', [CartController::class, 'index'])->name('cart.index');

Loading…
Cancel
Save