From 231a4511fdef6ded17b5659a0b31f13952145c85 Mon Sep 17 00:00:00 2001 From: Aji Kamaludin Date: Wed, 28 Jun 2023 13:28:50 +0700 Subject: [PATCH] penukaran poin --- .../Customer/PoinExchangeController.php | 102 ++++++++-------- app/Models/LocationProfile.php | 34 ++++++ app/Services/GeneralService.php | 17 ++- resources/js/Customer/Poin/Index.jsx | 2 +- .../Exchange.jsx => PoinExchange/Index.jsx} | 113 +++++++++++------- .../PoinExchange/IndexPartials/AllVoucher.jsx | 3 + .../IndexPartials/FavoriteVoucher.jsx | 3 + .../{Poin => PoinExchange}/VoucherCard.jsx | 41 ++++--- resources/js/Customer/Trx/Detail.jsx | 6 +- resources/js/Customer/utils.jsx | 24 ++++ resources/js/Layouts/CustomerLayout.jsx | 17 ++- resources/js/Pages/Sale/Detail.jsx | 4 +- resources/js/constant.js | 10 ++ routes/web.php | 2 +- 14 files changed, 248 insertions(+), 130 deletions(-) rename resources/js/Customer/{Poin/Exchange.jsx => PoinExchange/Index.jsx} (64%) create mode 100644 resources/js/Customer/PoinExchange/IndexPartials/AllVoucher.jsx create mode 100644 resources/js/Customer/PoinExchange/IndexPartials/FavoriteVoucher.jsx rename resources/js/Customer/{Poin => PoinExchange}/VoucherCard.jsx (72%) diff --git a/app/Http/Controllers/Customer/PoinExchangeController.php b/app/Http/Controllers/Customer/PoinExchangeController.php index 79f666f..bcc06b3 100644 --- a/app/Http/Controllers/Customer/PoinExchangeController.php +++ b/app/Http/Controllers/Customer/PoinExchangeController.php @@ -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) { - return $q->where('price_poin', '!=', 0); - }); + $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 ($request->location_ids != '') { - $vouchers->whereHas('locationProfile', function ($q) use ($request) { - return $q->whereIn('location_id', $request->location_ids); - }); - - $slocations = Location::whereIn('id', $request->location_ids)->get(); + if ($favorite == 0) { + if ($request->location_ids != '') { + $profiles->whereIn('location_id', $request->location_ids); + $profiles = $profiles->paginate(20); - $vouchers = tap($vouchers->paginate(20))->setHidden(['username', 'password']); + $slocations = Location::whereIn('id', $request->location_ids)->get(); + } } - 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); - $sale->items()->create([ - 'entity_type' => $voucher::class, - 'entity_id' => $voucher->id, - 'price' => $voucher->price_poin, - 'quantity' => 1, - 'additional_info_json' => json_encode([ - 'id' => $voucher->id, + $vouchers = $profile->shuffle_unsold(1); + foreach ($vouchers as $voucher) { + $sale->items()->create([ + 'entity_type' => $voucher::class, + 'entity_id' => $voucher->id, + 'price' => $voucher->validate_price_poin, 'quantity' => 1, - 'voucher' => $voucher->load(['location']), - ]), - ]); + 'additional_info_json' => json_encode([ + 'voucher' => $voucher->load(['locationProfile.location']) + ]), + ]); - $voucher->update(['is_sold' => Voucher::SOLD]); - $voucher->check_stock_notification(); + $voucher->update(['is_sold' => Voucher::SOLD]); + $voucher->check_stock_notification(); - $sale->create_notification(); + $poin = $customer->poins()->create([ + 'credit' => $voucher->validate_price_poin, + 'description' => $sale->code, + 'narration' => 'Penukaran Voucher Poin' + ]); - $poin = $customer->poins()->create([ - 'credit' => $voucher->price_poin, - 'description' => $sale->code, - ]); + $poin->update_customer_balance(); + } + + $sale->create_notification(); - $poin->update_customer_balance(); DB::commit(); return redirect()->route('transactions.sale.show', $sale) diff --git a/app/Models/LocationProfile.php b/app/Models/LocationProfile.php index 9648061..c56637c 100644 --- a/app/Models/LocationProfile.php +++ b/app/Models/LocationProfile.php @@ -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([ diff --git a/app/Services/GeneralService.php b/app/Services/GeneralService.php index d322eee..136cd1a 100644 --- a/app/Services/GeneralService.php +++ b/app/Services/GeneralService.php @@ -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) { @@ -185,12 +196,12 @@ class GeneralService } return $number; } - + public static function isAllowAffilate($key) { $isAllow = false; $levels = json_decode(Setting::getByKey('AFFILATE_ALLOWED_LEVELS')); - foreach($levels as $level) { + foreach ($levels as $level) { if ($key == $level->key) { $isAllow = true; } diff --git a/resources/js/Customer/Poin/Index.jsx b/resources/js/Customer/Poin/Index.jsx index 4382e03..669d572 100644 --- a/resources/js/Customer/Poin/Index.jsx +++ b/resources/js/Customer/Poin/Index.jsx @@ -77,7 +77,7 @@ export default function Index(props) { {_poins.length <= 0 && }
-
+
{_poins.length > 0 && (
{formatIDDate(dates.startDate)} s/d{' '} diff --git a/resources/js/Customer/Poin/Exchange.jsx b/resources/js/Customer/PoinExchange/Index.jsx similarity index 64% rename from resources/js/Customer/Poin/Exchange.jsx rename to resources/js/Customer/PoinExchange/Index.jsx index 6f83a88..2a5c928 100644 --- a/resources/js/Customer/Poin/Exchange.jsx +++ b/resources/js/Customer/PoinExchange/Index.jsx @@ -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) {
tukarkan poin anda dengan voucher manarik
+
@@ -124,34 +145,41 @@ export default function Exhange(props) {
-
- -
+ {favorite === ALL ? ( -
- {sLocations.map((location, index) => ( -
handleRemoveLocation(index)} - > -
{location.name}
-
- + <> +
+ +
+
+ {sLocations.map((location, index) => ( +
+ handleRemoveLocation(index) + } + > +
{location.name}
+
+ +
-
- ))} -
+ ))} +
+ ) : ( -
+
{fLocations.map((location, index) => (
handleRemoveLocation(index)} + onClick={() => + handleFavoriteRemoveLocation(location) + } >
{location.name}
@@ -162,17 +190,15 @@ export default function Exhange(props) {
)}
- {vouchers.length <= 0 ? ( + + {items.length <= 0 ? ( ) : (
{/* voucher */}
- {vouchers.map((voucher) => ( - + {items.map((item) => ( + ))} {next_page_url !== null && (
)}
+ All Voucher
+} diff --git a/resources/js/Customer/PoinExchange/IndexPartials/FavoriteVoucher.jsx b/resources/js/Customer/PoinExchange/IndexPartials/FavoriteVoucher.jsx new file mode 100644 index 0000000..61868df --- /dev/null +++ b/resources/js/Customer/PoinExchange/IndexPartials/FavoriteVoucher.jsx @@ -0,0 +1,3 @@ +export function FavoriteVoucher() { + return
All Voucher
+} diff --git a/resources/js/Customer/Poin/VoucherCard.jsx b/resources/js/Customer/PoinExchange/VoucherCard.jsx similarity index 72% rename from resources/js/Customer/Poin/VoucherCard.jsx rename to resources/js/Customer/PoinExchange/VoucherCard.jsx index bcf51f3..570e3e7 100644 --- a/resources/js/Customer/Poin/VoucherCard.jsx +++ b/resources/js/Customer/PoinExchange/VoucherCard.jsx @@ -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 ( setShow(false)}>
- {voucher.location_profile.name} + {item.location.name}
- {voucher.location_profile.display_note} + {item.display_note}
- {formatIDR(voucher.location_profile.price_poin)}{' '} - poin{' '} + {formatIDR(item.validate_price_poin)} poin{' '}
- {voucher.location_profile.quota} + {item.quota}
- {voucher.location_profile.display_expired} + {item.display_expired}
+ {item.display_note !== null && ( +
+ {item.display_note} +
+ )}
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)} > -
- {voucher.location_profile.location.name} -
+
{item.location.name}
- {voucher.location_profile.display_note} + {item.display_note}
- {formatIDR(voucher.validate_price_poin)} poin + {formatIDR(item.validate_price_poin)} poin
-
- {voucher.location_profile.quota} -
+
{item.quota}
- {voucher.location_profile.display_expired} + {item.display_expired}
- + ) } diff --git a/resources/js/Customer/Trx/Detail.jsx b/resources/js/Customer/Trx/Detail.jsx index ffe7be1..f15939f 100644 --- a/resources/js/Customer/Trx/Detail.jsx +++ b/resources/js/Customer/Trx/Detail.jsx @@ -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 }) {
TOTAL
- pembayaran:{' '} - {sale.payed_with === 'paylater' - ? 'saldo hutang' - : 'saldo deposit'} + {convertPayedWith(sale.payed_with)}
{sale.display_amount}
diff --git a/resources/js/Customer/utils.jsx b/resources/js/Customer/utils.jsx index edc5176..581de67 100644 --- a/resources/js/Customer/utils.jsx +++ b/resources/js/Customer/utils.jsx @@ -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
toast.dismiss(t.id)}>{message}
+ }) +} + +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' diff --git a/resources/js/Layouts/CustomerLayout.jsx b/resources/js/Layouts/CustomerLayout.jsx index 1368791..94e47cd 100644 --- a/resources/js/Layouts/CustomerLayout.jsx +++ b/resources/js/Layouts/CustomerLayout.jsx @@ -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 ( -
toast.dismiss(t.id)}> - {flash.message.message} -
- ) - }) + 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) diff --git a/resources/js/Pages/Sale/Detail.jsx b/resources/js/Pages/Sale/Detail.jsx index bc5e254..a091323 100644 --- a/resources/js/Pages/Sale/Detail.jsx +++ b/resources/js/Pages/Sale/Detail.jsx @@ -45,8 +45,8 @@ export default function Detail(props) { const { sale } = props return ( - - + +
diff --git a/resources/js/constant.js b/resources/js/constant.js index 42a4818..599be27 100644 --- a/resources/js/constant.js +++ b/resources/js/constant.js @@ -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' diff --git a/routes/web.php b/routes/web.php index 8a912db..51fc451 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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');