From 8ef3a2783afe29d077b6735bf5bc58746b395f46 Mon Sep 17 00:00:00 2001 From: Bayu Lukman Yusuf Date: Fri, 27 Feb 2026 11:57:16 +0700 Subject: [PATCH] discount - use point --- app/Http/Controllers/CartController.php | 2 +- app/Http/Controllers/CheckoutController.php | 194 ++++++-- .../Controllers/VoucherEventController.php | 2 +- lang/en/checkout.php | 12 + lang/id/checkout.php | 14 +- resources/views/checkout/v1-cart.blade.php | 15 +- .../checkout/v1-delivery-1-shipping.blade.php | 5 +- .../views/checkout/v1-delivery-1.blade.php | 42 +- .../checkout/order-summary.blade.php | 6 +- .../components/checkout/promo-code.blade.php | 124 ++++- .../components/checkout/use-point.blade.php | 451 ++++++++++++++++++ .../components/shop/add-to-cart.blade.php | 1 + routes/web.php | 4 + 13 files changed, 763 insertions(+), 109 deletions(-) create mode 100644 resources/views/components/checkout/use-point.blade.php diff --git a/app/Http/Controllers/CartController.php b/app/Http/Controllers/CartController.php index b2e0136..592c29a 100644 --- a/app/Http/Controllers/CartController.php +++ b/app/Http/Controllers/CartController.php @@ -35,7 +35,7 @@ class CartController extends Controller public function add(MemberCartRequest $request, MemberCartRepository $repository) { - Log::info($request->all()); + // Log::info($request->all()); $data = $request->validated(); $item = $repository->create($data); diff --git a/app/Http/Controllers/CheckoutController.php b/app/Http/Controllers/CheckoutController.php index 065cdce..80d9530 100644 --- a/app/Http/Controllers/CheckoutController.php +++ b/app/Http/Controllers/CheckoutController.php @@ -9,7 +9,6 @@ use App\Repositories\Member\ShippingRepository; use App\Repositories\Member\Transaction\TransactionRepository; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; - use Illuminate\Validation\ValidationException; class CheckoutController extends Controller @@ -22,22 +21,18 @@ class CheckoutController extends Controller $subtotal = $memberCartRepository->getSubtotal($request->input('location_id')); - $address_list = Address::where('user_id', auth()->user()->id)->orderBy('is_primary','desc')->get(); - + $address_list = Address::where('user_id', auth()->user()->id)->orderBy('is_primary', 'desc')->get(); $total = $subtotal; - - $carts = $memberCartRepository->getList($request); - return view('checkout.v1-delivery-1', [ 'carts' => $carts, 'subtotal' => $subtotal, 'total' => $total, 'store' => $store, - 'address_list' => $address_list, + 'address_list' => $address_list, ]); } @@ -46,33 +41,33 @@ class CheckoutController extends Controller $delivery_method = $request->input('delivery_method') ?? 'shipping'; $address_id = $request->input('address_id'); - if ($address_id == null) { - $address_list = Address::where('user_id', $request->user()->id)->orderBy('is_primary','desc')->get(); + $address_list = Address::where('user_id', $request->user()->id)->orderBy('is_primary', 'desc')->get(); $address_id = $address_list->first()->id; } if ($delivery_method == null || $address_id == null) { - + return redirect()->back()->with('error', 'Delivery method or address is required'); } if ($delivery_method == 'shipping') { session(['checkout_delivery_method' => $delivery_method]); session(['checkout_address_id' => $address_id]); + return redirect()->route('checkout.shipping'); } if ($delivery_method == 'pickup') { session(['checkout_delivery_method' => $delivery_method]); session(['checkout_address_id' => null]); + return redirect()->route('checkout.payment'); } return redirect()->back()->with('error', 'Delivery method is not valid'); } - public function chooseShipping(Request $request, MemberCartRepository $memberCartRepository, ShippingRepository $shippingRepository) { try { @@ -81,7 +76,6 @@ class CheckoutController extends Controller $location_id = session('location_id', 22); - if ($delivery_method == null || $address_id == null || $location_id == null) { return redirect()->route('checkout.delivery'); @@ -90,37 +84,43 @@ class CheckoutController extends Controller $subtotal = $memberCartRepository->getSubtotal($location_id); $total = $subtotal; - $request->merge(['location_id' => $location_id]); $carts = $memberCartRepository->getList($request); - - try{ + try { $shipping_list = collect($shippingRepository->getList([ - "location_id" => $location_id, - "address_id" => $address_id, - "items" => $carts, - ])['pricing'] ?? [])->map(function($row){ + 'location_id' => $location_id, + 'address_id' => $address_id, + 'items' => $carts, + ])['pricing'] ?? [])->map(function ($row) { return [ - 'courier' => $row['courier_code'], - 'service' => $row['courier_service_code'], - 'title' => $row['courier_name']." - ".$row['courier_service_name'], + 'courier' => $row['courier_code'], + 'service' => $row['courier_service_code'], + 'title' => $row['courier_name'].' - '.$row['courier_service_name'], 'description' => $row['duration'], - 'cost' => $row['shipping_fee'], + 'cost' => $row['shipping_fee'], ]; }); if (count($shipping_list) == 0) { throw ValidationException::withMessages([ - "message" => "Tidak dapat menghitung ongkir" + 'message' => 'Tidak dapat menghitung ongkir', ]); } - }catch(\Exception $e){ + } catch (\Exception $e) { + $message = $e->getMessage(); + + // if contain Failed due to invalid or missing postal code + if (str_contains($message, 'Failed due to invalid or missing postal code')) { + $message = 'Kode pos tidak valid atau tidak ditemukan'; + } else if (str_contains($message, 'support@biteship.com')) { + $message = 'Layanan pengiriman tidak tersedia untuk alamat ini'; + } throw ValidationException::withMessages([ - "message" => $e->getMessage() + 'message' => $message, ]); } - + return view('checkout.v1-delivery-1-shipping', [ 'carts' => $request->user()->carts, 'subtotal' => $subtotal, @@ -132,6 +132,7 @@ class CheckoutController extends Controller ]); } catch (\Exception $e) { Log::info($e); + return redirect()->route('checkout.delivery')->with('error', $e->getMessage() ?? 'Invalid checkout data'); } } @@ -164,6 +165,8 @@ class CheckoutController extends Controller $location_id = session('location_id', 22); + $use_point = session('use_point') ?? 0; + $items = []; $request->merge(['location_id' => $location_id]); @@ -172,25 +175,27 @@ class CheckoutController extends Controller if (count($carts) == 0) { return redirect()->route('checkout.delivery')->with('error', 'No items in cart'); } - + foreach ($carts as $cart) { $items[] = [ - "item_reference_id" => $cart->item_reference_id, - "qty" => $cart->qty + 'item_reference_id' => $cart->item_reference_id, + 'qty' => $cart->qty, ]; } $data = [ - "address_id" => $address_id, - "note" => "", - "courier_company" => $courier, - "courier_type" => $service, - "location_id" => $location_id, - "items" => $items, - "vouchers" => [], - "use_customer_points" => 0, + 'address_id' => $address_id, + 'note' => '', + 'courier_company' => $courier, + 'courier_type' => $service, + 'location_id' => $location_id, + 'items' => $items, + 'vouchers' => [], + 'use_customer_points' => $use_point ?? 0, ]; - + + dd($data); + $item = $repository->create($data); $notification = new \App\Notifications\Member\Transaction\OrderWaitPayment($item); @@ -201,19 +206,20 @@ class CheckoutController extends Controller // proses payment - - $payment = $item->payments()->where("method_type",'App\Models\XenditLink') - ->where("status",'PENDING') - ->first(); - $invoice_url = $payment ? @$payment->method->invoice_url: ""; - - return redirect()->to($invoice_url)->withHeaders([ - 'Cache-Control' => 'no-cache, no-store, must-revalidate', - 'Pragma' => 'no-cache', - 'Expires' => '0' - ]); + $payment = $item->payments()->where('method_type', 'App\Models\XenditLink') + ->where('status', 'PENDING') + ->first(); + $invoice_url = $payment ? @$payment->method->invoice_url : ''; + // reset state session + session()->forget(['checkout_delivery_method', 'checkout_address_id', 'checkout_courier', 'checkout_service', 'use_point']); + + return redirect()->to($invoice_url)->withHeaders([ + 'Cache-Control' => 'no-cache, no-store, must-revalidate', + 'Pragma' => 'no-cache', + 'Expires' => '0', + ]); } catch (\Exception $e) { @@ -222,4 +228,92 @@ class CheckoutController extends Controller return redirect()->route('checkout.delivery')->with('error', 'Invalid checkout data'); } } + + /** + * Apply points to session + */ + public function applyPoint(Request $request) + { + try { + $usePoint = $request->input('use_point'); + + // Validate input + if (!$usePoint || !is_numeric($usePoint) || $usePoint < 0) { + return response()->json([ + 'success' => false, + 'message' => 'Invalid points value' + ]); + } + + $usePoint = (int) $usePoint; + + // Get user's available points + $userPoints = auth()->user()->customer->point ?? 0; + + // Check if user has enough points + if ($usePoint > $userPoints) { + return response()->json([ + 'success' => false, + 'message' => 'You cannot use more points than available' + ]); + } + + // Set points in session + session(['use_point' => $usePoint]); + + return response()->json([ + 'success' => true, + 'message' => 'Points applied successfully', + 'points_applied' => $usePoint, + 'remaining_points' => $userPoints - $usePoint + ]); + + } catch (\Exception $e) { + Log::error('Error applying points: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while applying points' + ]); + } + } + + /** + * Remove points from session + */ + public function removePoint(Request $request) + { + try { + // Check if points are currently applied + $currentPoints = session('use_point', 0); + + if ($currentPoints <= 0) { + return response()->json([ + 'success' => false, + 'message' => 'No points are currently applied' + ]); + } + + // Remove points from session + session()->forget('use_point'); + + // Get user's available points + $userPoints = auth()->user()->customer->point ?? 0; + + return response()->json([ + 'success' => true, + 'message' => 'Points removed successfully', + 'points_removed' => $currentPoints, + 'available_points' => $userPoints + ]); + + } catch (\Exception $e) { + Log::error('Error removing points: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while removing points' + ]); + } + } } diff --git a/app/Http/Controllers/VoucherEventController.php b/app/Http/Controllers/VoucherEventController.php index 2dc23ce..02ea22d 100644 --- a/app/Http/Controllers/VoucherEventController.php +++ b/app/Http/Controllers/VoucherEventController.php @@ -39,7 +39,7 @@ class VoucherEventController extends Controller // Call the repository's redeem method $result = $this->voucherEventRepository->redeem($voucherEvent, $user); - if ($result['success']) { + if ($result) { return response()->json([ 'success' => true, 'message' => $result['message'] ?? 'Voucher redeemed successfully!', diff --git a/lang/en/checkout.php b/lang/en/checkout.php index fafb736..c943daf 100644 --- a/lang/en/checkout.php +++ b/lang/en/checkout.php @@ -56,4 +56,16 @@ return [ 'terms_and_conditions' => 'Terms and Conditions', 'close' => 'Close', 'redeem' => 'Redeem', + 'use_point' => 'Use Points', + 'your_points' => 'Your Points', + 'available_points' => 'Available Points', + 'input_point' => 'Enter points to use', + 'apply_point' => 'Apply Points', + 'remove_point' => 'Remove Points', + 'points_applied' => 'Points Applied', + 'points_used' => 'points used', + 'saving' => 'Discount:', + 'shipping' => 'Shipping:', + 'estimated_total' => 'Estimated total:', + 'proceed_to_checkout' => 'Proceed to checkout', ]; diff --git a/lang/id/checkout.php b/lang/id/checkout.php index 2020e48..04e4c3c 100644 --- a/lang/id/checkout.php +++ b/lang/id/checkout.php @@ -50,10 +50,22 @@ return [ 'enter_valid_promo_code' => 'Masukkan kode promo yang valid!', 'apply' => 'Gunakan', 'available_vouchers' => 'Voucher Tersedia', - 'available_voucher_events' => 'Event Voucher Tersedia', + 'available_voucher_events' => 'Tukar Poin', 'points_required' => 'Poin Diperlukan', 'valid_period' => 'Periode Berlaku', 'terms_and_conditions' => 'Syarat dan Ketentuan', 'close' => 'Tutup', 'redeem' => 'Tukar', + 'use_point' => 'Gunakan Poin', + 'your_points' => 'Poin Anda', + 'available_points' => 'Poin Tersedia', + 'input_point' => 'Masukkan poin yang akan digunakan', + 'apply_point' => 'Gunakan Poin', + 'remove_point' => 'Hapus Poin', + 'points_applied' => 'Poin Diterapkan', + 'points_used' => 'poin digunakan', + 'saving' => 'Diskon:', + 'shipping' => 'Pengiriman:', + 'estimated_total' => 'Total estimasi:', + 'proceed_to_checkout' => 'Lanjut ke checkout', ]; diff --git a/resources/views/checkout/v1-cart.blade.php b/resources/views/checkout/v1-cart.blade.php index 3aee0c1..8a2a074 100644 --- a/resources/views/checkout/v1-cart.blade.php +++ b/resources/views/checkout/v1-cart.blade.php @@ -186,26 +186,26 @@ Rp 0
  • - Saving: - Rp 0 + {{ __('checkout.saving') }} + Rp {{ number_format(session('use_point') ?? 0,0,",",".") }}
  • {{-- --}}
  • - Shipping: + {{ __('checkout.shipping') }} Calculated at checkout
  • - Estimated total: + {{ __('checkout.estimated_total') }} $0.00
    - Proceed to checkout + {{ __('checkout.proceed_to_checkout') }} {{--
    - + {{-- --}} + @endif @@ -1039,7 +1040,7 @@ } // Calculate estimated total (subtotal - savings + tax) - const savings = 0; // Fixed savings amount + const savings = {{ session('use_point') ?? 0 }}; // Fixed savings amount const tax = 0; // Fixed tax amount const estimatedTotal = subtotal - savings + tax; diff --git a/resources/views/checkout/v1-delivery-1-shipping.blade.php b/resources/views/checkout/v1-delivery-1-shipping.blade.php index beec72c..881f742 100644 --- a/resources/views/checkout/v1-delivery-1-shipping.blade.php +++ b/resources/views/checkout/v1-delivery-1-shipping.blade.php @@ -84,7 +84,7 @@ @@ -113,6 +113,7 @@ }); function updateOrderSummaryWithShippingCost(shippingValue) { + const saving = {{ session('use_point') ?? 0 }}; console.log('updateOrderSummaryWithShippingCost called with:', shippingValue); // Parse the shipping value: courier|service|cost @@ -138,7 +139,7 @@ const currentSubtotal = parseFloat(subtotalElement.textContent.replace(/[^\d]/g, '')); console.log('Current subtotal:', currentSubtotal); - const newTotal = currentSubtotal + cost; + const newTotal = currentSubtotal + cost - saving; console.log('New total:', newTotal); // Store original total diff --git a/resources/views/checkout/v1-delivery-1.blade.php b/resources/views/checkout/v1-delivery-1.blade.php index 4d856ad..6ebed3d 100644 --- a/resources/views/checkout/v1-delivery-1.blade.php +++ b/resources/views/checkout/v1-delivery-1.blade.php @@ -191,7 +191,7 @@ @@ -428,11 +428,12 @@ function updateOrderSummaryWithShipping() { // Simulate shipping cost calculation based on postcode const shippingCost = calculateShippingCost(document.getElementById('zip').value); + const saving = {{ session('use_point') ?? 0 }}; // Update order summary const subtotalElement = document.getElementById('cart-subtotal'); const currentSubtotal = parseFloat(subtotalElement.textContent.replace(/[^\d]/g, '')); - const newTotal = currentSubtotal + shippingCost; + const newTotal = currentSubtotal + 0 - saving; // Store original total if (!window.originalTotal) { @@ -481,40 +482,15 @@ } function showShippingCost(cost, isFree = false) { - // Find or create shipping cost element in order summary - let shippingElement = document.querySelector('[data-shipping-cost]'); - - if (!shippingElement) { - const orderSummary = document.querySelector('.list-unstyled'); - const shippingLi = document.createElement('li'); - shippingLi.className = 'd-flex justify-content-between'; - shippingLi.setAttribute('data-shipping-cost', ''); - shippingLi.innerHTML = ` - {{ __('checkout.shipping') }}: - Rp 0 - `; - orderSummary.appendChild(shippingLi); - shippingElement = shippingLi; - } - - const costElement = document.getElementById('shipping-cost'); - if (isFree) { - costElement.textContent = '{{ __('checkout.free') }}'; - costElement.className = 'text-success fw-medium'; - } else { - costElement.textContent = `Rp ${number_format(cost, 0, ',', '.')}`; - costElement.className = 'text-dark-emphasis fw-medium'; - } - - shippingElement.style.display = 'flex'; + } function validateDeliveryForm() { - const postcode = document.getElementById('zip').value; - const firstName = document.getElementById('firstName').value; - const address = document.getElementById('address').value; - const city = document.getElementById('city').value; - const phone = document.getElementById('phone').value; + const postcode = document.getElementById('zip').textContent; + const firstName = document.getElementById('firstName').textContent; + const address = document.getElementById('address').textContent; + const city = document.getElementById('city').textContent; + const phone = document.getElementById('phone').textContent; if (!postcode) { alert('{{ __('checkout.enter_postcode') }}'); diff --git a/resources/views/components/checkout/order-summary.blade.php b/resources/views/components/checkout/order-summary.blade.php index 7839e5d..3eb2579 100644 --- a/resources/views/components/checkout/order-summary.blade.php +++ b/resources/views/components/checkout/order-summary.blade.php @@ -1,7 +1,7 @@ @props([ 'subtotal' => 0, 'total' => 0, - 'savings' => 0, + 'savings' => session('use_point'), 'tax' => 0, 'showEdit' => false, 'editUrl' => null, @@ -36,9 +36,7 @@
    • -
      {{ __('cart_summary.subtotal') }} ( - {{ __('cart_summary.items_count', ['count' => auth()->check() ? \App\Repositories\Member\Cart\MemberCartRepository::getCount() : 0]) }}): -
      +
      {{ __('cart_summary.subtotal') }} ({{ __('cart_summary.items_count', ['count' => auth()->check() ? \App\Repositories\Member\Cart\MemberCartRepository::getCount() : 0]) }})
      Rp {{ number_format($subtotal, 0, ',', '.') }}
    • diff --git a/resources/views/components/checkout/promo-code.blade.php b/resources/views/components/checkout/promo-code.blade.php index 0f9abd4..35d8389 100644 --- a/resources/views/components/checkout/promo-code.blade.php +++ b/resources/views/components/checkout/promo-code.blade.php @@ -48,8 +48,13 @@
      @foreach($vouchers as $voucher) - - {{ $voucher->code }} + + {{ $voucher->description }} @endforeach
      @@ -187,6 +192,84 @@ document.addEventListener('DOMContentLoaded', function () { }); + // Handle promo code form submission + document.addEventListener('submit', function(e) { + const form = e.target; + + // Check if this is the promo code form + if (!form || !form.classList.contains('needs-validation')) { + return; + } + + e.preventDefault(); + + const promoInput = form.querySelector('input[placeholder="{{ __('checkout.enter_promo_code') }}"]'); + const submitBtn = form.querySelector('button[type="submit"]'); + const originalText = submitBtn.textContent; + + if (!promoInput || !promoInput.value.trim()) { + alert('Please enter a promo code'); + return; + } + + // Show loading state + submitBtn.disabled = true; + submitBtn.textContent = 'Applying...'; + + // Get CSRF token + const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); + + // Submit voucher code to server + fetch('/apply-voucher', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-CSRF-TOKEN': token, + 'X-Requested-With': 'XMLHttpRequest' + }, + body: new URLSearchParams({ + 'promo_code': promoInput.value.trim() + }).toString() + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // Show success message + alert('Voucher applied successfully!'); + + // Clear input + promoInput.value = ''; + + // Add visual feedback + promoInput.style.backgroundColor = '#d4edda'; + promoInput.style.borderColor = '#28a745'; + + // Remove visual feedback after 2 seconds + setTimeout(() => { + promoInput.style.backgroundColor = ''; + promoInput.style.borderColor = ''; + }, 2000); + + // Reload page to show updated cart + setTimeout(() => { + window.location.reload(); + }, 1000); + } else { + // Show error message + alert(data.message || 'Failed to apply voucher'); + } + }) + .catch(error => { + console.error('Error applying voucher:', error); + alert('An error occurred while applying the voucher'); + }) + .finally(() => { + // Restore button state + submitBtn.disabled = false; + submitBtn.textContent = originalText; + }); + }); + // Handle redeem button click document.getElementById('redeemVoucherBtn').addEventListener('click', function() { const voucherEventId = this.getAttribute('data-voucher-event-id'); @@ -215,14 +298,7 @@ document.addEventListener('DOMContentLoaded', function () { // Show success message alert('Voucher redeemed successfully!'); - // Close modal - const modal = bootstrap.Modal.getInstance(document.getElementById('voucherEventModal')); - modal.hide(); - - // Reload page to update cart - setTimeout(() => { - window.location.reload(); - }, 1000); + window.location.reload(); } else { // Show error message alert(data.message || 'Failed to redeem voucher'); @@ -259,6 +335,34 @@ document.addEventListener('DOMContentLoaded', function () { }); }); + // Function to apply voucher code when badge is clicked + function applyVoucher(voucherCode) { + // Find the promo code input + const promoInput = document.querySelector('input[placeholder="{{ __('checkout.enter_promo_code') }}"]'); + + if (promoInput) { + // Set the voucher code + promoInput.value = voucherCode; + + // Add visual feedback + promoInput.style.backgroundColor = '#d4edda'; + promoInput.style.borderColor = '#28a745'; + + // Remove visual feedback after 2 seconds + setTimeout(() => { + promoInput.style.backgroundColor = ''; + promoInput.style.borderColor = ''; + }, 2000); + + // Focus on the input + promoInput.focus(); + + // Optionally submit the form automatically + // Uncomment the next line if you want auto-submission + // promoInput.closest('form').requestSubmit(); + } + } + }); \ No newline at end of file diff --git a/resources/views/components/checkout/use-point.blade.php b/resources/views/components/checkout/use-point.blade.php new file mode 100644 index 0000000..39c439e --- /dev/null +++ b/resources/views/components/checkout/use-point.blade.php @@ -0,0 +1,451 @@ +
      +
      +

      + +

      + +
      + +
      + + {{-- Your Points --}} +
      + +
      +
      +

      {{ number_format(auth()->user()->customer->point, 0, ",",".") }}

      + {{ __('checkout.available_points') }} +
      + +
      +
      + + {{-- Points Form --}} + @if(session('use_point')) + {{-- Points Applied Display --}} +
      +
      +
      {{ __('checkout.points_applied') }}
      +

      + {{ number_format(session('use_point'), 0, ",", ".") }} {{ __('checkout.points_used') }} +

      +
      + +
      + @else + {{-- Apply Points Form --}} +
      +
      + +
      + +
      + @endif + + + +
      +
      +
      +
      + + + +{{-- SINGLE REUSABLE MODAL --}} + + + + + \ No newline at end of file diff --git a/resources/views/components/shop/add-to-cart.blade.php b/resources/views/components/shop/add-to-cart.blade.php index 1266610..3dd5437 100644 --- a/resources/views/components/shop/add-to-cart.blade.php +++ b/resources/views/components/shop/add-to-cart.blade.php @@ -133,6 +133,7 @@ showLoginDialog(); } else { // You could show an error message here + alert(error.message); } } } catch (error) { diff --git a/routes/web.php b/routes/web.php index 24d76f5..67bb21f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -123,6 +123,10 @@ Route::middleware(['auth'])->prefix('/checkout')->group(function () { }); +// Apply points route (outside checkout prefix for easier access) +Route::post('/apply-points', [CheckoutController::class, 'applyPoint'])->name('checkout.apply.points'); +Route::post('/remove-points', [CheckoutController::class, 'removePoint'])->name('checkout.remove.points'); + Route::middleware(['auth'])->prefix('/voucher-events')->group(function () { Route::post('/{voucherEvent}/redeem', [VoucherEventController::class, 'redeem'])->name('voucher-events.redeem'); });