discount - use point

This commit is contained in:
Bayu Lukman Yusuf 2026-02-27 11:57:16 +07:00
parent a60ca44ca1
commit 8ef3a2783a
13 changed files with 763 additions and 109 deletions

View File

@ -35,7 +35,7 @@ class CartController extends Controller
public function add(MemberCartRequest $request, MemberCartRepository $repository) public function add(MemberCartRequest $request, MemberCartRepository $repository)
{ {
Log::info($request->all()); // Log::info($request->all());
$data = $request->validated(); $data = $request->validated();
$item = $repository->create($data); $item = $repository->create($data);

View File

@ -9,7 +9,6 @@ use App\Repositories\Member\ShippingRepository;
use App\Repositories\Member\Transaction\TransactionRepository; use App\Repositories\Member\Transaction\TransactionRepository;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
class CheckoutController extends Controller class CheckoutController extends Controller
@ -22,22 +21,18 @@ class CheckoutController extends Controller
$subtotal = $memberCartRepository->getSubtotal($request->input('location_id')); $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; $total = $subtotal;
$carts = $memberCartRepository->getList($request); $carts = $memberCartRepository->getList($request);
return view('checkout.v1-delivery-1', [ return view('checkout.v1-delivery-1', [
'carts' => $carts, 'carts' => $carts,
'subtotal' => $subtotal, 'subtotal' => $subtotal,
'total' => $total, 'total' => $total,
'store' => $store, 'store' => $store,
'address_list' => $address_list, 'address_list' => $address_list,
]); ]);
} }
@ -46,9 +41,8 @@ class CheckoutController extends Controller
$delivery_method = $request->input('delivery_method') ?? 'shipping'; $delivery_method = $request->input('delivery_method') ?? 'shipping';
$address_id = $request->input('address_id'); $address_id = $request->input('address_id');
if ($address_id == null) { 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; $address_id = $address_list->first()->id;
} }
@ -60,19 +54,20 @@ class CheckoutController extends Controller
if ($delivery_method == 'shipping') { if ($delivery_method == 'shipping') {
session(['checkout_delivery_method' => $delivery_method]); session(['checkout_delivery_method' => $delivery_method]);
session(['checkout_address_id' => $address_id]); session(['checkout_address_id' => $address_id]);
return redirect()->route('checkout.shipping'); return redirect()->route('checkout.shipping');
} }
if ($delivery_method == 'pickup') { if ($delivery_method == 'pickup') {
session(['checkout_delivery_method' => $delivery_method]); session(['checkout_delivery_method' => $delivery_method]);
session(['checkout_address_id' => null]); session(['checkout_address_id' => null]);
return redirect()->route('checkout.payment'); return redirect()->route('checkout.payment');
} }
return redirect()->back()->with('error', 'Delivery method is not valid'); return redirect()->back()->with('error', 'Delivery method is not valid');
} }
public function chooseShipping(Request $request, MemberCartRepository $memberCartRepository, ShippingRepository $shippingRepository) public function chooseShipping(Request $request, MemberCartRepository $memberCartRepository, ShippingRepository $shippingRepository)
{ {
try { try {
@ -81,7 +76,6 @@ class CheckoutController extends Controller
$location_id = session('location_id', 22); $location_id = session('location_id', 22);
if ($delivery_method == null || $address_id == null || $location_id == null) { if ($delivery_method == null || $address_id == null || $location_id == null) {
return redirect()->route('checkout.delivery'); return redirect()->route('checkout.delivery');
@ -90,34 +84,40 @@ class CheckoutController extends Controller
$subtotal = $memberCartRepository->getSubtotal($location_id); $subtotal = $memberCartRepository->getSubtotal($location_id);
$total = $subtotal; $total = $subtotal;
$request->merge(['location_id' => $location_id]); $request->merge(['location_id' => $location_id]);
$carts = $memberCartRepository->getList($request); $carts = $memberCartRepository->getList($request);
try {
try{
$shipping_list = collect($shippingRepository->getList([ $shipping_list = collect($shippingRepository->getList([
"location_id" => $location_id, 'location_id' => $location_id,
"address_id" => $address_id, 'address_id' => $address_id,
"items" => $carts, 'items' => $carts,
])['pricing'] ?? [])->map(function($row){ ])['pricing'] ?? [])->map(function ($row) {
return [ return [
'courier' => $row['courier_code'], 'courier' => $row['courier_code'],
'service' => $row['courier_service_code'], 'service' => $row['courier_service_code'],
'title' => $row['courier_name']." - ".$row['courier_service_name'], 'title' => $row['courier_name'].' - '.$row['courier_service_name'],
'description' => $row['duration'], 'description' => $row['duration'],
'cost' => $row['shipping_fee'], 'cost' => $row['shipping_fee'],
]; ];
}); });
if (count($shipping_list) == 0) { if (count($shipping_list) == 0) {
throw ValidationException::withMessages([ 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([ throw ValidationException::withMessages([
"message" => $e->getMessage() 'message' => $message,
]); ]);
} }
@ -132,6 +132,7 @@ class CheckoutController extends Controller
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
Log::info($e); Log::info($e);
return redirect()->route('checkout.delivery')->with('error', $e->getMessage() ?? 'Invalid checkout data'); 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); $location_id = session('location_id', 22);
$use_point = session('use_point') ?? 0;
$items = []; $items = [];
$request->merge(['location_id' => $location_id]); $request->merge(['location_id' => $location_id]);
@ -175,22 +178,24 @@ class CheckoutController extends Controller
foreach ($carts as $cart) { foreach ($carts as $cart) {
$items[] = [ $items[] = [
"item_reference_id" => $cart->item_reference_id, 'item_reference_id' => $cart->item_reference_id,
"qty" => $cart->qty 'qty' => $cart->qty,
]; ];
} }
$data = [ $data = [
"address_id" => $address_id, 'address_id' => $address_id,
"note" => "", 'note' => '',
"courier_company" => $courier, 'courier_company' => $courier,
"courier_type" => $service, 'courier_type' => $service,
"location_id" => $location_id, 'location_id' => $location_id,
"items" => $items, 'items' => $items,
"vouchers" => [], 'vouchers' => [],
"use_customer_points" => 0, 'use_customer_points' => $use_point ?? 0,
]; ];
dd($data);
$item = $repository->create($data); $item = $repository->create($data);
$notification = new \App\Notifications\Member\Transaction\OrderWaitPayment($item); $notification = new \App\Notifications\Member\Transaction\OrderWaitPayment($item);
@ -201,19 +206,20 @@ class CheckoutController extends Controller
// proses payment // proses payment
$payment = $item->payments()->where('method_type', 'App\Models\XenditLink')
$payment = $item->payments()->where("method_type",'App\Models\XenditLink') ->where('status', 'PENDING')
->where("status",'PENDING') ->first();
->first(); $invoice_url = $payment ? @$payment->method->invoice_url : '';
$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'
]);
// 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) { } catch (\Exception $e) {
@ -222,4 +228,92 @@ class CheckoutController extends Controller
return redirect()->route('checkout.delivery')->with('error', 'Invalid checkout data'); 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'
]);
}
}
} }

View File

@ -39,7 +39,7 @@ class VoucherEventController extends Controller
// Call the repository's redeem method // Call the repository's redeem method
$result = $this->voucherEventRepository->redeem($voucherEvent, $user); $result = $this->voucherEventRepository->redeem($voucherEvent, $user);
if ($result['success']) { if ($result) {
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'message' => $result['message'] ?? 'Voucher redeemed successfully!', 'message' => $result['message'] ?? 'Voucher redeemed successfully!',

View File

@ -56,4 +56,16 @@ return [
'terms_and_conditions' => 'Terms and Conditions', 'terms_and_conditions' => 'Terms and Conditions',
'close' => 'Close', 'close' => 'Close',
'redeem' => 'Redeem', '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',
]; ];

View File

@ -50,10 +50,22 @@ return [
'enter_valid_promo_code' => 'Masukkan kode promo yang valid!', 'enter_valid_promo_code' => 'Masukkan kode promo yang valid!',
'apply' => 'Gunakan', 'apply' => 'Gunakan',
'available_vouchers' => 'Voucher Tersedia', 'available_vouchers' => 'Voucher Tersedia',
'available_voucher_events' => 'Event Voucher Tersedia', 'available_voucher_events' => 'Tukar Poin',
'points_required' => 'Poin Diperlukan', 'points_required' => 'Poin Diperlukan',
'valid_period' => 'Periode Berlaku', 'valid_period' => 'Periode Berlaku',
'terms_and_conditions' => 'Syarat dan Ketentuan', 'terms_and_conditions' => 'Syarat dan Ketentuan',
'close' => 'Tutup', 'close' => 'Tutup',
'redeem' => 'Tukar', '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',
]; ];

View File

@ -186,26 +186,26 @@
<span class="text-dark-emphasis fw-medium" id="cart-subtotal">Rp 0</span> <span class="text-dark-emphasis fw-medium" id="cart-subtotal">Rp 0</span>
</li> </li>
<li class="d-flex justify-content-between"> <li class="d-flex justify-content-between">
Saving: {{ __('checkout.saving') }}
<span class="text-danger fw-medium">Rp 0</span> <span class="text-danger fw-medium">Rp {{ number_format(session('use_point') ?? 0,0,",",".") }}</span>
</li> </li>
{{-- <li class="d-flex justify-content-between" id="tax-row" style="display: none;"> {{-- <li class="d-flex justify-content-between" id="tax-row" style="display: none;">
Tax collected: Tax collected:
<span class="text-dark-emphasis fw-medium">Rp 0</span> <span class="text-dark-emphasis fw-medium">Rp 0</span>
</li> --}} </li> --}}
<li class="d-flex justify-content-between"> <li class="d-flex justify-content-between">
Shipping: {{ __('checkout.shipping') }}
<span class="text-dark-emphasis fw-medium">Calculated at checkout</span> <span class="text-dark-emphasis fw-medium">Calculated at checkout</span>
</li> </li>
</ul> </ul>
<div class="border-top pt-4 mt-4"> <div class="border-top pt-4 mt-4">
<div class="d-flex justify-content-between mb-3"> <div class="d-flex justify-content-between mb-3">
<span class="fs-sm">Estimated total:</span> <span class="fs-sm">{{ __('checkout.estimated_total') }}</span>
<span class="h5 mb-0" id="cart-estimated-total">$0.00</span> <span class="h5 mb-0" id="cart-estimated-total">$0.00</span>
</div> </div>
<a class="btn btn-lg btn-primary w-100" <a class="btn btn-lg btn-primary w-100"
href="{{ route('checkout.delivery') }}"> href="{{ route('checkout.delivery') }}">
Proceed to checkout {{ __('checkout.proceed_to_checkout') }}
<i class="ci-chevron-right fs-lg ms-1 me-n1"></i> <i class="ci-chevron-right fs-lg ms-1 me-n1"></i>
</a> </a>
{{-- <div class="nav justify-content-center fs-sm mt-3"> {{-- <div class="nav justify-content-center fs-sm mt-3">
@ -217,7 +217,8 @@
</div> </div>
</div> </div>
</div> </div>
<x-checkout.promo-code /> {{-- <x-checkout.promo-code /> --}}
<x-checkout.use-point />
</div> </div>
</aside> </aside>
@endif @endif
@ -1039,7 +1040,7 @@
} }
// Calculate estimated total (subtotal - savings + tax) // 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 tax = 0; // Fixed tax amount
const estimatedTotal = subtotal - savings + tax; const estimatedTotal = subtotal - savings + tax;

View File

@ -84,7 +84,7 @@
<!-- Order summary (sticky sidebar) --> <!-- Order summary (sticky sidebar) -->
<aside class="col-lg-4 offset-xl-1" style="margin-top: -100px"> <aside class="col-lg-4 offset-xl-1" style="margin-top: -100px">
<div class="position-sticky top-0" style="padding-top: 100px"> <div class="position-sticky top-0" style="padding-top: 100px">
<x-checkout.order-summary :subtotal="$subtotal" :total="$total" :savings="0" :tax="0" <x-checkout.order-summary :subtotal="$subtotal" :total="$total"
:showEdit="true" :editUrl="route('cart.index')" /> :showEdit="true" :editUrl="route('cart.index')" />
</div> </div>
</aside> </aside>
@ -113,6 +113,7 @@
}); });
function updateOrderSummaryWithShippingCost(shippingValue) { function updateOrderSummaryWithShippingCost(shippingValue) {
const saving = {{ session('use_point') ?? 0 }};
console.log('updateOrderSummaryWithShippingCost called with:', shippingValue); console.log('updateOrderSummaryWithShippingCost called with:', shippingValue);
// Parse the shipping value: courier|service|cost // Parse the shipping value: courier|service|cost
@ -138,7 +139,7 @@
const currentSubtotal = parseFloat(subtotalElement.textContent.replace(/[^\d]/g, '')); const currentSubtotal = parseFloat(subtotalElement.textContent.replace(/[^\d]/g, ''));
console.log('Current subtotal:', currentSubtotal); console.log('Current subtotal:', currentSubtotal);
const newTotal = currentSubtotal + cost; const newTotal = currentSubtotal + cost - saving;
console.log('New total:', newTotal); console.log('New total:', newTotal);
// Store original total // Store original total

View File

@ -191,7 +191,7 @@
<!-- Order summary (sticky sidebar) --> <!-- Order summary (sticky sidebar) -->
<aside class="col-lg-4 offset-xl-1" style="margin-top: -100px"> <aside class="col-lg-4 offset-xl-1" style="margin-top: -100px">
<div class="position-sticky top-0" style="padding-top: 100px"> <div class="position-sticky top-0" style="padding-top: 100px">
<x-checkout.order-summary :subtotal="$subtotal" :total="$total" :savings="0" :tax="0" <x-checkout.order-summary :subtotal="$subtotal" :total="$total"
:showEdit="true" :editUrl="route('cart.index')" /> :showEdit="true" :editUrl="route('cart.index')" />
</div> </div>
</aside> </aside>
@ -428,11 +428,12 @@
function updateOrderSummaryWithShipping() { function updateOrderSummaryWithShipping() {
// Simulate shipping cost calculation based on postcode // Simulate shipping cost calculation based on postcode
const shippingCost = calculateShippingCost(document.getElementById('zip').value); const shippingCost = calculateShippingCost(document.getElementById('zip').value);
const saving = {{ session('use_point') ?? 0 }};
// Update order summary // Update order summary
const subtotalElement = document.getElementById('cart-subtotal'); const subtotalElement = document.getElementById('cart-subtotal');
const currentSubtotal = parseFloat(subtotalElement.textContent.replace(/[^\d]/g, '')); const currentSubtotal = parseFloat(subtotalElement.textContent.replace(/[^\d]/g, ''));
const newTotal = currentSubtotal + shippingCost; const newTotal = currentSubtotal + 0 - saving;
// Store original total // Store original total
if (!window.originalTotal) { if (!window.originalTotal) {
@ -481,40 +482,15 @@
} }
function showShippingCost(cost, isFree = false) { 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 = `
<span>{{ __('checkout.shipping') }}:</span>
<span class="text-dark-emphasis fw-medium" id="shipping-cost">Rp 0</span>
`;
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() { function validateDeliveryForm() {
const postcode = document.getElementById('zip').value; const postcode = document.getElementById('zip').textContent;
const firstName = document.getElementById('firstName').value; const firstName = document.getElementById('firstName').textContent;
const address = document.getElementById('address').value; const address = document.getElementById('address').textContent;
const city = document.getElementById('city').value; const city = document.getElementById('city').textContent;
const phone = document.getElementById('phone').value; const phone = document.getElementById('phone').textContent;
if (!postcode) { if (!postcode) {
alert('{{ __('checkout.enter_postcode') }}'); alert('{{ __('checkout.enter_postcode') }}');

View File

@ -1,7 +1,7 @@
@props([ @props([
'subtotal' => 0, 'subtotal' => 0,
'total' => 0, 'total' => 0,
'savings' => 0, 'savings' => session('use_point'),
'tax' => 0, 'tax' => 0,
'showEdit' => false, 'showEdit' => false,
'editUrl' => null, 'editUrl' => null,
@ -36,9 +36,7 @@
</div> </div>
<ul class="list-unstyled fs-sm gap-3 mb-0"> <ul class="list-unstyled fs-sm gap-3 mb-0">
<li class="d-flex justify-content-between"> <li class="d-flex justify-content-between">
<div>{{ __('cart_summary.subtotal') }} ( <div>{{ __('cart_summary.subtotal') }} ({{ __('cart_summary.items_count', ['count' => auth()->check() ? \App\Repositories\Member\Cart\MemberCartRepository::getCount() : 0]) }})</div>
{{ __('cart_summary.items_count', ['count' => auth()->check() ? \App\Repositories\Member\Cart\MemberCartRepository::getCount() : 0]) }}):
</div>
<span class="text-dark-emphasis fw-medium" id="cart-subtotal">Rp <span class="text-dark-emphasis fw-medium" id="cart-subtotal">Rp
{{ number_format($subtotal, 0, ',', '.') }}</span> {{ number_format($subtotal, 0, ',', '.') }}</span>
</li> </li>

View File

@ -48,8 +48,13 @@
<div class="d-flex flex-wrap gap-2"> <div class="d-flex flex-wrap gap-2">
@foreach($vouchers as $voucher) @foreach($vouchers as $voucher)
<span class="badge bg-light text-dark px-3 py-2"> <span class="badge bg-light text-dark px-3 py-2"
{{ $voucher->code }} style="cursor: pointer; transition: all 0.2s ease;"
onclick="applyVoucher('{{ $voucher->code }}')"
onmouseover="this.style.backgroundColor='#e9ecef'; this.style.transform='scale(1.05)';"
onmouseout="this.style.backgroundColor='#f8f9fa'; this.style.transform='scale(1)';"
title="Click to apply {{ $voucher->code }}">
{{ $voucher->description }}
</span> </span>
@endforeach @endforeach
</div> </div>
@ -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 // Handle redeem button click
document.getElementById('redeemVoucherBtn').addEventListener('click', function() { document.getElementById('redeemVoucherBtn').addEventListener('click', function() {
const voucherEventId = this.getAttribute('data-voucher-event-id'); const voucherEventId = this.getAttribute('data-voucher-event-id');
@ -215,14 +298,7 @@ document.addEventListener('DOMContentLoaded', function () {
// Show success message // Show success message
alert('Voucher redeemed successfully!'); alert('Voucher redeemed successfully!');
// Close modal window.location.reload();
const modal = bootstrap.Modal.getInstance(document.getElementById('voucherEventModal'));
modal.hide();
// Reload page to update cart
setTimeout(() => {
window.location.reload();
}, 1000);
} else { } else {
// Show error message // Show error message
alert(data.message || 'Failed to redeem voucher'); 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();
}
}
}); });
</script> </script>

View File

@ -0,0 +1,451 @@
<div class="accordion bg-body-tertiary rounded-5 p-4">
<div class="accordion-item border-0">
<h3 class="accordion-header" id="promoCodeHeading">
<button type="button"
class="accordion-button animate-underline collapsed py-0 ps-sm-2 ps-lg-0 ps-xl-2"
data-bs-toggle="collapse"
data-bs-target="#promoCode"
aria-expanded="false"
aria-controls="promoCode">
<i class="ci-percent fs-xl me-2"></i>
<span class="animate-target me-2">
{{ __('checkout.use_point') }}
</span>
</button>
</h3>
<div class="accordion-collapse collapse"
id="promoCode"
aria-labelledby="promoCodeHeading">
<div class="accordion-body pt-3 pb-2 ps-sm-2 px-lg-0 px-xl-2">
{{-- Your Points --}}
<div class="alert bg-white dark:bg-gray-800 text-white mb-3">
<div class="d-flex align-items-center gap-3">
<div>
<h4 class="mb-0 fw-bold text-dark dark:text-white" id="userPoints">{{ number_format(auth()->user()->customer->point, 0, ",",".") }}</h4>
<small class="text-black dark:text-white">{{ __('checkout.available_points') }}</small>
</div>
</div>
</div>
{{-- Points Form --}}
@if(session('use_point'))
{{-- Points Applied Display --}}
<div class="alert alert-success d-flex align-items-center gap-3">
<div class="flex-grow-1">
<h6 class="mb-1">{{ __('checkout.points_applied') }}</h6>
<p class="mb-0">
<strong>{{ number_format(session('use_point'), 0, ",", ".") }}</strong> {{ __('checkout.points_used') }}
</p>
</div>
<button type="button" class="btn btn-outline-danger btn-sm" id="remove-points-btn">
{{ __('checkout.remove_point') }}
</button>
</div>
@else
{{-- Apply Points Form --}}
<form class="needs-validation d-flex gap-2" novalidate>
<div class="position-relative w-100">
<input type="number"
class="form-control"
id="input-point"
min="1"
max="{{ auth()->user()->customer->point }}"
placeholder="{{ __('checkout.input_point') }}"
required>
</div>
<button type="submit" class="btn btn-dark">
{{ __('checkout.apply_point') }}
</button>
</form>
@endif
</div>
</div>
</div>
</div>
{{-- SINGLE REUSABLE MODAL --}}
<div class="modal fade" id="voucherEventModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content rounded-4">
<div class="modal-header border-0">
<h5 class="modal-title fw-semibold">
Voucher Event
</h5>
<button type="button"
class="btn-close"
data-bs-dismiss="modal">
</button>
</div>
<div class="modal-body pt-0">
<div id="voucherEventContent"></div>
</div>
<div class="modal-footer border-0">
<button type="button"
class="btn btn-secondary"
data-bs-dismiss="modal">
{{ __('checkout.close') }}
</button>
<button type="button"
class="btn btn-primary"
id="redeemVoucherBtn"
data-voucher-event-id="">
{{ __('checkout.redeem') }}
</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const modal = document.getElementById('voucherEventModal');
modal.addEventListener('show.bs.modal', function (event) {
const button = event.relatedTarget;
const name = button.getAttribute('data-name');
const description = button.getAttribute('data-description');
const point = button.getAttribute('data-point');
const start = button.getAttribute('data-start');
const end = button.getAttribute('data-end');
const terms = button.getAttribute('data-terms');
let content = '';
if (description) {
content += `<p>${description}</p>`;
}
if (point) {
content += `
<div class="mb-2">
<strong>{{ __('checkout.points_required') }}:</strong>
${point}
</div>
`;
}
if (start && end) {
content += `
<div class="mb-2">
<strong>{{ __('checkout.valid_period') }}:</strong><br>
${start} - ${end}
</div>
`;
}
if (terms) {
content += `
<div class="mt-3 small text-muted">
<strong>{{ __('checkout.terms_and_conditions') }}:</strong>
<p>${terms}</p>
</div>
`;
}
modal.querySelector('.modal-title').textContent = name;
document.getElementById('voucherEventContent').innerHTML = content;
// Set voucher event ID for redeem button
const redeemBtn = document.getElementById('redeemVoucherBtn');
redeemBtn.setAttribute('data-voucher-event-id', button.getAttribute('data-id'));
});
// Handle points form submission
document.addEventListener('submit', function(e) {
const form = e.target;
// Check if this is the points form
if (!form || !form.classList.contains('needs-validation')) {
return;
}
e.preventDefault();
const pointInput = form.querySelector('input[placeholder="{{ __('checkout.input_point') }}"]');
const submitBtn = form.querySelector('button[type="submit"]');
const originalText = submitBtn.textContent;
const availablePoints = parseInt(document.getElementById('userPoints').textContent.replace(/\./g, ''));
if (!pointInput || !pointInput.value.trim()) {
alert('Please enter points to use');
return;
}
const pointsToUse = parseInt(pointInput.value.trim());
if (isNaN(pointsToUse) || pointsToUse <= 0) {
alert('Please enter a valid number of points');
return;
}
if (pointsToUse > availablePoints) {
alert('You cannot use more points than available');
return;
}
// Show loading state
submitBtn.disabled = true;
submitBtn.textContent = 'Applying...';
// Get CSRF token
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
// Submit points to server to set session
fetch('/apply-points', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': token,
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({
'use_point': pointsToUse
}).toString()
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Show success message
alert('Points applied successfully!');
// Clear input
pointInput.value = '';
// Add visual feedback
pointInput.style.backgroundColor = '#d4edda';
pointInput.style.borderColor = '#28a745';
// Remove visual feedback after 2 seconds
setTimeout(() => {
pointInput.style.backgroundColor = '';
pointInput.style.borderColor = '';
}, 2000);
// Reload page to show updated cart
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
// Show error message
alert(data.message || 'Failed to apply points');
}
})
.catch(error => {
console.error('Error applying points:', error);
alert('An error occurred while applying the points');
})
.finally(() => {
// Restore button state
submitBtn.disabled = false;
submitBtn.textContent = originalText;
});
});
// Handle remove points button click
document.getElementById('remove-points-btn')?.addEventListener('click', function() {
const removeBtn = this;
const originalText = removeBtn.textContent;
// Show loading state
removeBtn.disabled = true;
removeBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Removing...';
// Get CSRF token
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
// Remove points via AJAX
fetch('/remove-points', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': token,
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Show success message
alert('Points removed successfully!');
// Clear input field
const pointInput = document.querySelector('input#input-point');
if (pointInput) {
pointInput.value = '';
pointInput.style.backgroundColor = '#f8d7da';
pointInput.style.borderColor = '#dc3545';
// Remove visual feedback after 2 seconds
setTimeout(() => {
pointInput.style.backgroundColor = '';
pointInput.style.borderColor = '';
}, 2000);
}
// Reload page to show updated cart
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
// Show error message
alert(data.message || 'Failed to remove points');
}
})
.catch(error => {
console.error('Error removing points:', error);
alert('An error occurred while removing the points');
})
.finally(() => {
// Restore button state
removeBtn.disabled = false;
removeBtn.textContent = originalText;
});
});
// Handle redeem button click
document.getElementById('redeemVoucherBtn').addEventListener('click', function() {
const voucherEventId = this.getAttribute('data-voucher-event-id');
if (!voucherEventId) {
console.error('No voucher event ID found');
return;
}
// Disable button during processing
this.disabled = true;
this.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Processing...';
// Make AJAX call to redeem voucher
fetch(`/voucher-events/${voucherEventId}/redeem`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
},
body: JSON.stringify({})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Show success message
alert('Voucher redeemed successfully!');
window.location.reload();
} else {
// Show error message
alert(data.message || 'Failed to redeem voucher');
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while redeeming the voucher');
})
.finally(() => {
// Re-enable button
this.disabled = false;
this.innerHTML = '{{ __('checkout.redeem') }}';
});
});
// Custom modal handler to prevent backdrop issues
const voucherBadges = document.querySelectorAll('[data-bs-toggle="modal"][data-bs-target="#voucherEventModal"]');
voucherBadges.forEach(function(badge) {
badge.addEventListener('click', function(e) {
e.preventDefault();
// Remove any existing backdrop
const existingBackdrop = document.querySelector('.modal-backdrop');
if (existingBackdrop) {
existingBackdrop.remove();
}
// Show modal without backdrop
const modalInstance = new bootstrap.Modal(modal, {
backdrop: false
});
modalInstance.show();
});
});
// Function to apply voucher code when badge is clicked
function applyVoucher(voucherCode) {
// Find the point input
const pointInput = document.querySelector('input#input-point');
if (pointInput) {
// set point session via AJAX
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
fetch('/apply-points', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': token,
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({
'use_point': voucherCode
}).toString()
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Set the voucher code in input
pointInput.value = voucherCode;
// Add visual feedback
pointInput.style.backgroundColor = '#d4edda';
pointInput.style.borderColor = '#28a745';
// Remove visual feedback after 2 seconds
setTimeout(() => {
pointInput.style.backgroundColor = '';
pointInput.style.borderColor = '';
}, 2000);
// Show success message
alert('Points applied successfully!');
// Reload page to show updated cart
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
// Show error message
alert(data.message || 'Failed to apply points');
}
})
.catch(error => {
console.error('Error applying points:', error);
alert('An error occurred while applying the points');
});
// Focus on the input
pointInput.focus();
}
}
});
</script>

View File

@ -133,6 +133,7 @@
showLoginDialog(); showLoginDialog();
} else { } else {
// You could show an error message here // You could show an error message here
alert(error.message);
} }
} }
} catch (error) { } catch (error) {

View File

@ -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::middleware(['auth'])->prefix('/voucher-events')->group(function () {
Route::post('/{voucherEvent}/redeem', [VoucherEventController::class, 'redeem'])->name('voucher-events.redeem'); Route::post('/{voucherEvent}/redeem', [VoucherEventController::class, 'redeem'])->name('voucher-events.redeem');
}); });