list voucher & fix address

This commit is contained in:
Bayu Lukman Yusuf 2026-02-18 12:57:30 +07:00
parent 9ec2918fb4
commit d58c8d7ca2
22 changed files with 619 additions and 71 deletions

View File

@ -21,6 +21,7 @@ class AddressController extends Controller
return [ return [
'id' => $address->id, 'id' => $address->id,
'label' => $address->label, 'label' => $address->label,
'name' => $address->name,
'location' => $address->location, 'location' => $address->location,
'address' => $address->address, 'address' => $address->address,
'is_primary' => $address->is_primary, 'is_primary' => $address->is_primary,
@ -29,6 +30,8 @@ class AddressController extends Controller
'district_id' => $address->district_id, 'district_id' => $address->district_id,
'subdistrict_id' => $address->subdistrict_id, 'subdistrict_id' => $address->subdistrict_id,
'postal_code' => $address->postal_code, 'postal_code' => $address->postal_code,
'latitude' => $address->latitude,
'longitude' => $address->longitude,
]; ];
}); });
@ -82,25 +85,35 @@ class AddressController extends Controller
public function update(Request $request, $id) public function update(Request $request, $id)
{ {
$request->validate([ $request->validate([
'label' => 'required|string|max:255',
'name' => 'required|string|max:255',
'phone' => 'required|string|max:255',
'province_id' => 'required|exists:provinces,id', 'province_id' => 'required|exists:provinces,id',
'city_id' => 'required|exists:cities,id', 'city_id' => 'required|exists:cities,id',
'district_id' => 'required|exists:districts,id', 'district_id' => 'required|exists:districts,id',
'subdistrict_id' => 'required|exists:subdistricts,id', 'subdistrict_id' => 'required|exists:subdistricts,id',
'postal_code' => 'required|string|max:10', 'postal_code' => 'required|string|max:10',
'address' => 'required|string|max:255', 'address' => 'required|string|max:255',
'latitude' => 'required|numeric|between:-90,90',
'longitude' => 'required|numeric|between:-180,180',
'is_primary' => 'boolean' 'is_primary' => 'boolean'
]); ]);
$address = auth()->user()->addresses()->findOrFail($id); $address = auth()->user()->addresses()->findOrFail($id);
$address->update([ $address->update([
'label' => $request->label,
'name' => $request->name,
'phone' => $request->phone,
'province_id' => $request->province_id, 'province_id' => $request->province_id,
'city_id' => $request->city_id, 'city_id' => $request->city_id,
'district_id' => $request->district_id, 'district_id' => $request->district_id,
'subdistrict_id' => $request->subdistrict_id, 'subdistrict_id' => $request->subdistrict_id,
'postal_code' => $request->postal_code, 'postal_code' => $request->postal_code,
'address' => $request->address, 'address' => $request->address,
'is_primary' => $request->boolean('is_primary') 'latitude' => $request->latitude,
'longitude' => $request->longitude,
'is_primary' => $request->is_primary ?? $address->is_primary,
]); ]);
// Update location names based on selected IDs // Update location names based on selected IDs
@ -133,12 +146,17 @@ class AddressController extends Controller
public function store(Request $request) public function store(Request $request)
{ {
$request->validate([ $request->validate([
'label' => 'required|string|max:255',
'name' => 'required|string|max:255',
'phone' => 'required|string|max:255',
'province_id' => 'required|exists:provinces,id', 'province_id' => 'required|exists:provinces,id',
'city_id' => 'required|exists:cities,id', 'city_id' => 'required|exists:cities,id',
'district_id' => 'required|exists:districts,id', 'district_id' => 'required|exists:districts,id',
'subdistrict_id' => 'required|exists:subdistricts,id', 'subdistrict_id' => 'required|exists:subdistricts,id',
'postal_code' => 'required|string|max:10',
'address' => 'required|string|max:255', 'address' => 'required|string|max:255',
'postal_code' => 'required|string|max:10',
'latitude' => 'nullable|numeric|between:-90,90',
'longitude' => 'nullable|numeric|between:-180,180',
'is_primary' => 'boolean' 'is_primary' => 'boolean'
]); ]);
@ -149,18 +167,18 @@ class AddressController extends Controller
$subdistrict = Subdistrict::find($request->subdistrict_id); $subdistrict = Subdistrict::find($request->subdistrict_id);
$address = auth()->user()->addresses()->create([ $address = auth()->user()->addresses()->create([
'label' => 'Address ' . (auth()->user()->addresses()->count() + 1), 'label' => $request->label,
'name' => $request->name,
'phone' => $request->phone,
'province_id' => $request->province_id, 'province_id' => $request->province_id,
'city_id' => $request->city_id, 'city_id' => $request->city_id,
'district_id' => $request->district_id, 'district_id' => $request->district_id,
'subdistrict_id' => $request->subdistrict_id, 'subdistrict_id' => $request->subdistrict_id,
'province_name' => $province?->name,
'regency_name' => $city?->name,
'district_name' => $district?->name,
'village_name' => $subdistrict?->name,
'postal_code' => $request->postal_code,
'address' => $request->address, 'address' => $request->address,
'is_primary' => $request->boolean('is_primary') 'postal_code' => $request->postal_code,
'latitude' => $request->latitude,
'longitude' => $request->longitude,
'is_primary' => $request->is_primary ?? false,
]); ]);
// If set as primary, unset other primary addresses // If set as primary, unset other primary addresses

View File

@ -46,7 +46,7 @@ class ProfileController extends Controller
if ($request->hasFile('photo')) { if ($request->hasFile('photo')) {
$ext = $request->file('photo')->extension(); $ext = $request->file('photo')->extension();
$filename = $request->file('photo')->storeAs("profile", $user->id.".".$ext, "public"); $filename = $request->file('photo')->storeAs("profile", $user->id.".".$ext, "public");
$user->photo = $filename; $user->photo = asset('storage/' . $filename);
} }
$user->save(); $user->save();

View File

@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers;
use App\Repositories\Member\VoucherEvent\VoucherEventRepository;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
class VoucherEventController extends Controller
{
protected $voucherEventRepository;
public function __construct(VoucherEventRepository $voucherEventRepository)
{
$this->voucherEventRepository = $voucherEventRepository;
}
/**
* Redeem a voucher event
*
* @param int $voucherEvent
* @param Request $request
* @return JsonResponse
*/
public function redeem($voucherEvent, Request $request): JsonResponse
{
try {
// Get the authenticated user
$user = auth()->user();
if (!$user) {
return response()->json([
'success' => false,
'message' => 'User not authenticated'
], 401);
}
// Call the repository's redeem method
$result = $this->voucherEventRepository->redeem($voucherEvent, $user);
if ($result['success']) {
return response()->json([
'success' => true,
'message' => $result['message'] ?? 'Voucher redeemed successfully!',
'data' => $result['data'] ?? null
]);
} else {
return response()->json([
'success' => false,
'message' => $result['message'] ?? 'Failed to redeem voucher'
], 400);
}
} catch (\Exception $e) {
Log::error($e);
return response()->json([
'success' => false,
'message' => $e->getMessage()
], 500);
}
}
}

View File

@ -57,6 +57,7 @@ class User extends Authenticatable
return $this->hasOne(Customer::class); return $this->hasOne(Customer::class);
} }
public function addresses() public function addresses()
{ {
return $this->hasMany(Address::class); return $this->hasMany(Address::class);
@ -64,6 +65,6 @@ class User extends Authenticatable
public function getPhotoUrlAttribute() public function getPhotoUrlAttribute()
{ {
return $this->photo ? env('WMS_ASSET_URL') . '/' . $this->photo : asset('img/photo-placeholder.png'); return $this->photo ? (str_contains($this->photo, 'http') ? $this->photo : env('WMS_ASSET_URL') . '/' . $this->photo) : asset('img/photo-placeholder.png');
} }
} }

View File

@ -451,4 +451,5 @@ class MemberAuthRepository
return $user; return $user;
} }
} }

View File

@ -19,7 +19,7 @@ class VoucherRepository
$model = Voucher::where('customer_id', (int) @$customer->id) $model = Voucher::where('customer_id', (int) @$customer->id)
->where('used_at', null) ->where('used_at', null)
->where('expired_at','>=',Carbon::now()) ->where('expired_at','>=',Carbon::now())
->orderBy('created_at','desc')->paginate($request->limit); ->orderBy('created_at','desc')->paginate($request->limit ?? 100);
return $model; return $model;
} }

View File

@ -15,7 +15,7 @@ class VoucherEventRepository
{ {
public function getList($request) public function getList($request)
{ {
$model = VoucherEvent::where('redeem_point', '>', 0)->orderBy('created_at','desc')->paginate($request->limit); $model = VoucherEvent::where('redeem_point', '>', 0)->orderBy('created_at','desc')->paginate($request->limit ?? 100);
return $model; return $model;
} }
@ -114,7 +114,12 @@ class VoucherEventRepository
$nominal = $voucher->nominal; $nominal = $voucher->nominal;
} }
$customer = Customer::where('user_id', auth()->user()->id)->firstOrFail(); $customer = auth()->user()->customer ?? null;
if ($customer == null) {
throw ValidationException::withMessages([
"customer" => "Lengkapi profil terlebih dahulu"
]);
}
$point = DB::select("SELECT SUM(point) as point FROM customer_points WHERE customer_id = ? ", [$customer->id]); $point = DB::select("SELECT SUM(point) as point FROM customer_points WHERE customer_id = ? ", [$customer->id]);
$point = (float) @$point[0]->point; $point = (float) @$point[0]->point;
@ -141,7 +146,7 @@ class VoucherEventRepository
'user_id' => auth()->user()->id 'user_id' => auth()->user()->id
]); ]);
$customer = Customer::where('user_id', auth()->user()->id)->firstOrFail(); $customer = auth()->user()->customer;
$this->redeemPoint($customer->id, $voucher->redeem_point, "Redeem point for ".$model->description, $model); $this->redeemPoint($customer->id, $voucher->redeem_point, "Redeem point for ".$model->description, $model);

View File

@ -0,0 +1,39 @@
<?php
namespace App\View\Components\Checkout;
use App\Repositories\Member\VoucherEvent\VoucherEventRepository;
use App\Repositories\Member\Voucher\VoucherRepository;
use Illuminate\View\Component;
use Illuminate\View\View;
class PromoCode extends Component
{
public $voucher_events;
public $vouchers;
/**
* Create the component instance.
*
* @param VoucherEventRepository $voucherEventRepository
* @return void
*/
public function __construct(VoucherRepository $voucherRepository, VoucherEventRepository $voucherEventRepository)
{
$this->voucher_events = $voucherEventRepository->getList([]);
$this->vouchers = $voucherRepository->getList([]);
}
/**
* Get the view / contents that represent the component.
*
* @return View|string
*/
public function render()
{
return view('components.checkout.promo-code', [
'voucher_events' => $this->voucher_events,
'vouchers' => $this->vouchers
]);
}
}

View File

@ -17,6 +17,14 @@ return [
'select_village' => 'Select subdistrict...', 'select_village' => 'Select subdistrict...',
'zip_code' => 'ZIP code', 'zip_code' => 'ZIP code',
'address' => 'Address', 'address' => 'Address',
'label' => 'Label',
'name' => 'Name',
'phone' => 'Phone',
'please_enter_phone' => 'Please enter your phone number!',
'latitude' => 'Latitude',
'longitude' => 'Longitude',
'please_enter_latitude' => 'Please enter your latitude!',
'please_enter_longitude' => 'Please enter your longitude!',
'set_as_primary_address' => 'Set as primary address', 'set_as_primary_address' => 'Set as primary address',
'save_changes' => 'Save changes', 'save_changes' => 'Save changes',
'close' => 'Close', 'close' => 'Close',
@ -29,6 +37,8 @@ return [
'please_select_village' => 'Please select your village!', 'please_select_village' => 'Please select your village!',
'please_enter_zip_code' => 'Please enter your ZIP code!', 'please_enter_zip_code' => 'Please enter your ZIP code!',
'please_enter_address' => 'Please enter your address!', 'please_enter_address' => 'Please enter your address!',
'please_enter_label' => 'Please enter your label!',
'please_enter_name' => 'Please enter your name!',
'saving' => 'Saving', 'saving' => 'Saving',
'error_saving_address' => 'Error saving address', 'error_saving_address' => 'Error saving address',
'address_updated_successfully' => 'Address updated successfully', 'address_updated_successfully' => 'Address updated successfully',

View File

@ -45,4 +45,15 @@ return [
'pickup' => 'Pickup', 'pickup' => 'Pickup',
'pickup_ready' => 'Your order will be ready for pickup at the selected store', 'pickup_ready' => 'Your order will be ready for pickup at the selected store',
'continue_to_payment' => 'Continue to Payment', 'continue_to_payment' => 'Continue to Payment',
'apply_promo_code' => 'Apply promo code',
'enter_promo_code' => 'Enter promo code',
'enter_valid_promo_code' => 'Enter a valid promo code!',
'apply' => 'Apply',
'available_vouchers' => 'Available Vouchers',
'available_voucher_events' => 'Available Voucher Events',
'points_required' => 'Points Required',
'valid_period' => 'Valid Period',
'terms_and_conditions' => 'Terms and Conditions',
'close' => 'Close',
'redeem' => 'Redeem',
]; ];

View File

@ -17,7 +17,15 @@ return [
'select_village' => 'Pilih kelurahan/desa...', 'select_village' => 'Pilih kelurahan/desa...',
'zip_code' => 'Kode Pos', 'zip_code' => 'Kode Pos',
'address' => 'Alamat', 'address' => 'Alamat',
'set_as_primary_address' => 'Jadikan alamat utama', 'label' => 'Label',
'name' => 'Nama',
'phone' => 'Telepon',
'please_enter_phone' => 'Silakan masukkan nomor telepon Anda!',
'latitude' => 'Lintang',
'longitude' => 'Bujur',
'please_enter_latitude' => 'Silakan masukkan lintang Anda!',
'please_enter_longitude' => 'Silakan masukkan bujur Anda!',
'set_as_primary_address' => 'Jadikan sebagai alamat utama',
'save_changes' => 'Simpan perubahan', 'save_changes' => 'Simpan perubahan',
'close' => 'Tutup', 'close' => 'Tutup',
'alternative_shipping_address' => 'Alamat Pengiriman Alternatif', 'alternative_shipping_address' => 'Alamat Pengiriman Alternatif',
@ -29,6 +37,8 @@ return [
'please_select_village' => 'Silakan pilih kelurahan/desa Anda!', 'please_select_village' => 'Silakan pilih kelurahan/desa Anda!',
'please_enter_zip_code' => 'Silakan masukkan kode pos Anda!', 'please_enter_zip_code' => 'Silakan masukkan kode pos Anda!',
'please_enter_address' => 'Silakan masukkan alamat Anda!', 'please_enter_address' => 'Silakan masukkan alamat Anda!',
'please_enter_label' => 'Silakan masukkan label Anda!',
'please_enter_name' => 'Silakan masukkan nama Anda!',
'saving' => 'Menyimpan', 'saving' => 'Menyimpan',
'error_saving_address' => 'Terjadi kesalahan saat menyimpan alamat', 'error_saving_address' => 'Terjadi kesalahan saat menyimpan alamat',
'address_updated_successfully' => 'Alamat berhasil diperbarui', 'address_updated_successfully' => 'Alamat berhasil diperbarui',

View File

@ -45,4 +45,15 @@ return [
'pickup' => 'Ambil di Toko', 'pickup' => 'Ambil di Toko',
'pickup_ready' => 'Pesanan Anda akan siap diambil di toko yang dipilih', 'pickup_ready' => 'Pesanan Anda akan siap diambil di toko yang dipilih',
'continue_to_payment' => 'Lanjut ke Pembayaran', 'continue_to_payment' => 'Lanjut ke Pembayaran',
'apply_promo_code' => 'Gunakan kode promo',
'enter_promo_code' => 'Masukkan kode promo',
'enter_valid_promo_code' => 'Masukkan kode promo yang valid!',
'apply' => 'Gunakan',
'available_vouchers' => 'Voucher Tersedia',
'available_voucher_events' => 'Event Voucher Tersedia',
'points_required' => 'Poin Diperlukan',
'valid_period' => 'Periode Berlaku',
'terms_and_conditions' => 'Syarat dan Ketentuan',
'close' => 'Tutup',
'redeem' => 'Tukar',
]; ];

View File

@ -6,9 +6,18 @@
export default (() => { export default (() => {
const htmlElement = document.documentElement const htmlElement = document.documentElement
// Check the 'data-pwa' attribute of the HTML element // Check if Bootstrap is loaded
const checkBootstrapLoaded = () => {
if (typeof bootstrap === 'undefined') {
console.error('Bootstrap is not loaded');
return false;
}
return true;
};
// Check 'data-pwa' attribute of HTML element
if (htmlElement.getAttribute('data-pwa') !== 'true') { if (htmlElement.getAttribute('data-pwa') !== 'true') {
// Unregister the service worker if it's registered and 'data-pwa' is not 'true' // Unregister service worker if it's registered and 'data-pwa' is not 'true'
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then((registrations) => { navigator.serviceWorker.getRegistrations().then((registrations) => {
for (let registration of registrations) { for (let registration of registrations) {
@ -19,6 +28,11 @@ export default (() => {
return // Stop further execution to prevent PWA setup when not specified return // Stop further execution to prevent PWA setup when not specified
} }
// Check Bootstrap before proceeding with any Bootstrap-dependent code
if (!checkBootstrapLoaded()) {
return;
}
// Settings // Settings
const SETTINGS = { const SETTINGS = {
appName: 'AsiaGolf', appName: 'AsiaGolf',
@ -171,9 +185,16 @@ export default (() => {
// Get prompt instance // Get prompt instance
const promptElement = document.getElementById(promptId) const promptElement = document.getElementById(promptId)
// Check if Bootstrap is loaded before using it
if (!checkBootstrapLoaded()) {
console.error('Cannot create PWA prompt: Bootstrap is not loaded');
return;
}
/* eslint-disable no-undef */ /* eslint-disable no-undef */
const promptInstance = new bootstrap.Modal(promptElement, { const promptInstance = new bootstrap.Modal(promptElement, {
backdrop: 'static', // Optional: makes prompt not close when clicking outside backdrop: false, // Disable backdrop to prevent layering issues
keyboard: false, // Optional: makes prompt not close when pressing escape key keyboard: false, // Optional: makes prompt not close when pressing escape key
}) })
/* eslint-enable no-undef */ /* eslint-enable no-undef */

View File

@ -6,6 +6,8 @@
* @author Coderthemes * @author Coderthemes
* @version 1.0.0 * @version 1.0.0
*/ */
import 'bootstrap'
import 'img-comparison-slider' import 'img-comparison-slider'
import 'simplebar' import 'simplebar'

View File

@ -101,6 +101,32 @@
<div class="alert alert-danger d-none new-address-form-message" role="alert"></div> <div class="alert alert-danger d-none new-address-form-message" role="alert"></div>
<div class="alert alert-success d-none new-address-form-success" role="alert"></div> <div class="alert alert-success d-none new-address-form-success" role="alert"></div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.label') }}</label>
<input type="text" class="form-control new-label" id="new-label" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_label') }}</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.name') }}</label>
<input type="text" class="form-control new-name" id="new-name" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_name') }}</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.phone') }}</label>
<input type="tel" class="form-control new-phone" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_phone') }}</div>
</div>
</div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="position-relative"> <div class="position-relative">
<label class="form-label">{{ __('addresses.province') }}</label> <label class="form-label">{{ __('addresses.province') }}</label>
@ -140,17 +166,33 @@
<div class="invalid-feedback">{{ __('addresses.please_select_village') }}</div> <div class="invalid-feedback">{{ __('addresses.please_select_village') }}</div>
</div> </div>
</div> </div>
<div class="col-sm-4"> <div class="col-sm-6">
<div class="position-relative"> <div class="position-relative">
<label for="new-zip" class="form-label">{{ __('addresses.zip_code') }}</label> <label class="form-label">{{ __('addresses.zip_code') }}</label>
<input type="text" class="form-control" id="new-zip" required> <input type="text" class="form-control new-zip" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_zip_code') }}</div> <div class="invalid-feedback">{{ __('addresses.please_enter_zip_code') }}</div>
</div> </div>
</div> </div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.latitude') }}</label>
<input type="text" class="form-control new-latitude" step="0.000001" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_latitude') }}</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.longitude') }}</label>
<input type="text" class="form-control new-longitude" step="0.000001" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_longitude') }}</div>
</div>
</div>
<div class="col-sm-8"> <div class="col-sm-8">
<div class="position-relative"> <div class="position-relative">
<label for="new-address" class="form-label">{{ __('addresses.address') }}</label> <label for="new-address" class="form-label">{{ __('addresses.address') }}</label>
<input type="text" class="form-control" id="new-address" required> <textarea class="form-control" id="new-address" rows="3" required></textarea>
<div class="invalid-feedback">{{ __('addresses.please_enter_address') }}</div> <div class="invalid-feedback">{{ __('addresses.please_enter_address') }}</div>
</div> </div>
</div> </div>
@ -315,6 +357,7 @@
<ul class="list-unstyled fs-sm m-0"> <ul class="list-unstyled fs-sm m-0">
<li>${address.location}</li> <li>${address.location}</li>
<li>${address.address}</li> <li>${address.address}</li>
</ul> </ul>
</div> </div>
<div class="collapse primary-address-${address.id}" id="primaryAddressEdit${address.id}"> <div class="collapse primary-address-${address.id}" id="primaryAddressEdit${address.id}">
@ -325,6 +368,32 @@
<input type="hidden" name="_method" value="PUT"> <input type="hidden" name="_method" value="PUT">
<input type="hidden" name="_token" value="${document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''}"> <input type="hidden" name="_token" value="${document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''}">
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.label') }}</label>
<input type="text" class="form-control edit-label" value="${address.label}" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_address') }}</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.name') }}</label>
<input type="text" class="form-control edit-name" value="${address.name ?? ''}" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_name') }}</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.phone') }}</label>
<input type="tel" class="form-control edit-phone" value="${address.phone ?? ''}" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_phone') }}</div>
</div>
</div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="position-relative"> <div class="position-relative">
<label class="form-label">{{ __('addresses.province') }}</label> <label class="form-label">{{ __('addresses.province') }}</label>
@ -370,25 +439,41 @@
</div> </div>
</div> </div>
<div class="col-sm-4"> <div class="col-sm-6">
<div class="position-relative"> <div class="position-relative">
<label class="form-label">{{ __('addresses.zip_code') }}</label> <label class="form-label">{{ __('addresses.zip_code') }}</label>
<input type="text" class="form-control postal_code" value="${address.postal_code}" required> <input type="text" class="form-control postal_code" value="${address.postal_code}" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_zip_code') }}</div> <div class="invalid-feedback">{{ __('addresses.please_enter_zip_code') }}</div>
</div> </div>
</div> </div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.latitude') }}</label>
<input type="text" class="form-control latitude" value="${address.latitude ?? ''}" step="0.000001" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_latitude') }}</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.longitude') }}</label>
<input type="text" class="form-control longitude" value="${address.longitude ?? ''}" step="0.000001" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_longitude') }}</div>
</div>
</div>
<div class="col-sm-8"> <div class="col-sm-8">
<div class="position-relative"> <div class="position-relative">
<label class="form-label">{{ __('addresses.address') }}</label> <label class="form-label">{{ __('addresses.address') }}</label>
<input type="text" class="form-control zip_code" value="${address.address}" required> <input type="text" class="form-control address-input" value="${address.address}" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_address') }}</div> <div class="invalid-feedback">{{ __('addresses.please_enter_address') }}</div>
</div> </div>
</div> </div>
<div class="col-12"> <div class="col-12">
<div class="form-check mb-0"> <div class="form-check mb-0">
<input type="checkbox" class="form-check-input" ${address.is_primary ? 'checked' : ''}> <input type="checkbox" class="form-check-input " id="set-primary-${address.id}" ${address.is_primary ? 'checked' : ''}>
<label class="form-check-label">{{ __('addresses.set_as_primary_address') }}</label> <label class="form-check-label">{{ __('addresses.set_as_primary_address') }}</label>
</div> </div>
</div> </div>
@ -942,6 +1027,22 @@
// Get CSRF token // Get CSRF token
const tokenInput = form.querySelector('input[name="_token"]'); const tokenInput = form.querySelector('input[name="_token"]');
if (tokenInput) submitData._token = tokenInput.value; if (tokenInput) submitData._token = tokenInput.value;
// Get Name
const nameInput = isNewAddress ?
form.querySelector('#new-name') : form.querySelector('.edit-name');
if (nameInput) submitData.name = nameInput.value;
// Get Phone
const phoneInput = isNewAddress ?
form.querySelector('.new-phone') : form.querySelector('.edit-phone');
if (phoneInput) submitData.phone = phoneInput.value;
// Get Label
const labelInput = isNewAddress ?
form.querySelector('#new-label') : form.querySelector('.edit-label');
if (labelInput) submitData.label = labelInput.value;
// Get province ID // Get province ID
const provinceSelect = isNewAddress ? const provinceSelect = isNewAddress ?
@ -968,9 +1069,19 @@
form.querySelector('#new-zip') : form.querySelector('.postal_code'); form.querySelector('#new-zip') : form.querySelector('.postal_code');
if (zipInput) submitData.postal_code = zipInput.value; if (zipInput) submitData.postal_code = zipInput.value;
// Get address // Get latitude
const latitudeInput = isNewAddress ?
form.querySelector('.new-latitude') : form.querySelector('.latitude');
if (latitudeInput) submitData.latitude = latitudeInput.value;
// Get longitude
const longitudeInput = isNewAddress ?
form.querySelector('.new-longitude') : form.querySelector('.longitude');
if (longitudeInput) submitData.longitude = longitudeInput.value;
// Get address
const addressInput = isNewAddress ? const addressInput = isNewAddress ?
form.querySelector('#new-address') : form.querySelector('.zip_code'); form.querySelector('#new-address') : form.querySelector('.address-input');
if (addressInput) submitData.address = addressInput.value; if (addressInput) submitData.address = addressInput.value;
// Get primary checkbox // Get primary checkbox

View File

@ -38,8 +38,9 @@
<div class="position-relative"> <div class="position-relative">
<div class="avatar avatar-lg" style="aspect-ratio: 1/1; overflow: hidden; max-width: 300px; width: 100%; height: auto;"> <div class="avatar avatar-lg" style="aspect-ratio: 1/1; overflow: hidden; max-width: 300px; width: 100%; height: auto;">
@if(auth()->user()->photo) @if(auth()->user()->photo)
<img src="{{ asset('storage/' . auth()->user()->photo) }}" alt="{{ auth()->user()->name }}" class="avatar-img rounded-circle" style="width: 100%; height: 100%; object-fit: cover;" onerror="this.src='{{ asset('img/photo-placeholder.png') }}'"> <img src="{{ auth()->user()->photo_url }}" alt="{{ auth()->user()->name }}" class="avatar-img rounded-circle" style="width: 100%; height: 100%; object-fit: cover;" onerror="this.src='{{ asset('img/photo-placeholder.png') }}'">
@else @else
<img src="{{ asset('img/photo-placeholder.png') }}" alt="{{ auth()->user()->name }}" class="avatar-img rounded-circle" style="width: 100%; height: 100%; object-fit: cover;"> <img src="{{ asset('img/photo-placeholder.png') }}" alt="{{ auth()->user()->name }}" class="avatar-img rounded-circle" style="width: 100%; height: 100%; object-fit: cover;">
@endif @endif

View File

@ -47,8 +47,7 @@
<tr> <tr>
<th scope="col" class="fs-sm fw-normal py-3 ps-0"><span <th scope="col" class="fs-sm fw-normal py-3 ps-0"><span
class="text-body">Product</span></th> class="text-body">Product</span></th>
<th scope="col" class="text-body fs-sm fw-normal py-3 d-none d-xl-table-cell"><span
class="text-body">Price</span></th>
<th scope="col" class="text-body fs-sm fw-normal py-3 d-none d-md-table-cell"><span <th scope="col" class="text-body fs-sm fw-normal py-3 d-none d-md-table-cell"><span
class="text-body">Quantity</span></th> class="text-body">Quantity</span></th>
<th scope="col" class="text-body fs-sm fw-normal py-3 d-none d-md-table-cell"><span <th scope="col" class="text-body fs-sm fw-normal py-3 d-none d-md-table-cell"><span
@ -86,8 +85,10 @@
<a class="flex-shrink-0" <a class="flex-shrink-0"
href="{{ route('product.detail', $cart->itemVariant->item->slug) }}"> href="{{ route('product.detail', $cart->itemVariant->item->slug) }}">
<img src="{{ $cart->itemReference->item->image_url ?? '' }}" <img src="{{ $cart->itemReference->item->image_url ?? '' }}"
width="110" alt="{{ $cart->name ?? 'Product' }}"> width="80" alt="{{ $cart->name ?? 'Product' }}">
</a> </a>
<div class="w-100 min-w-0 ps-2 ps-xl-3"> <div class="w-100 min-w-0 ps-2 ps-xl-3">
<h5 class="d-flex animate-underline mb-2"> <h5 class="d-flex animate-underline mb-2">
<a class="d-block fs-sm fw-medium text-truncate animate-target" <a class="d-block fs-sm fw-medium text-truncate animate-target"
@ -103,6 +104,11 @@
<span class="text-dark-emphasis fw-medium">${{ number_format($cart->itemReference->display_price ?? 0, 2) }}</span> <span class="text-dark-emphasis fw-medium">${{ number_format($cart->itemReference->display_price ?? 0, 2) }}</span>
</li> </li>
</ul> --}} </ul> --}}
<input type="hidden" id="price_{{ $cart->id }}"
value="{{ $cart->itemVariant->display_price ?? 0 }}">
Rp {{ number_format($cart->itemVariant->display_price ?? 0, 0, ',', '.') }}
<div class="count-input rounded-2 d-md-none mt-3"> <div class="count-input rounded-2 d-md-none mt-3">
<button type="button" class="btn btn-sm btn-icon" <button type="button" class="btn btn-sm btn-icon"
data-decrement aria-label="Decrement quantity"> data-decrement aria-label="Decrement quantity">
@ -118,11 +124,7 @@
</div> </div>
</div> </div>
</td> </td>
<td class="h6 py-3 d-none d-xl-table-cell">
<input type="hidden" id="price_{{ $cart->id }}"
value="{{ $cart->itemVariant->display_price ?? 0 }}">
Rp {{ number_format($cart->itemVariant->display_price ?? 0, 0, ',', '.') }}
</td>
<td class="py-3 d-none d-md-table-cell"> <td class="py-3 d-none d-md-table-cell">
<div class="count-input"> <div class="count-input">
<button type="button" class="btn btn-icon" data-decrement <button type="button" class="btn btn-icon" data-decrement
@ -215,34 +217,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="accordion bg-body-tertiary rounded-5 p-4"> <x-checkout.promo-code />
<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">Apply promo code</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">
<form class="needs-validation d-flex gap-2" novalidate>
<div class="position-relative w-100">
<input type="text" class="form-control"
placeholder="Enter promo code" required>
<div class="invalid-tooltip bg-transparent py-0">Enter a valid promo
code!
</div>
</div>
<button type="submit" class="btn btn-dark">Apply</button>
</form>
</div>
</div>
</div>
</div>
</div> </div>
</aside> </aside>
@endif @endif
@ -251,7 +226,7 @@
<!-- Trending products (Carousel) --> <!-- Trending products (Carousel) -->
<section class="container pb-4 pb-md-5 mb-2 mb-sm-0 mb-lg-2 mb-xl-4"> {{-- <section class="container pb-4 pb-md-5 mb-2 mb-sm-0 mb-lg-2 mb-xl-4">
<h2 class="h3 border-bottom pb-4 mb-0">Trending products</h2> <h2 class="h3 border-bottom pb-4 mb-0">Trending products</h2>
<!-- Product carousel --> <!-- Product carousel -->
@ -681,7 +656,7 @@
</button> </button>
</div> </div>
</div> </div>
</section> </section> --}}
<!-- Subscription form + Vlog --> <!-- Subscription form + Vlog -->
@ -804,7 +779,7 @@
</div> </div>
</div> </div>
@include('layouts.partials/footer') @include('layouts.partials/footer2')
@endsection @endsection
@section('scripts') @section('scripts')

View File

@ -0,0 +1,264 @@
<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.apply_promo_code') }}
</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">
{{-- Promo Code Form --}}
<form class="needs-validation d-flex gap-2" novalidate>
<div class="position-relative w-100">
<input type="text"
class="form-control"
placeholder="{{ __('checkout.enter_promo_code') }}"
required>
<div class="invalid-tooltip bg-transparent py-0">
{{ __('checkout.enter_valid_promo_code') }}
</div>
</div>
<button type="submit" class="btn btn-dark">
{{ __('checkout.apply') }}
</button>
</form>
{{-- List Voucher --}}
@if($vouchers && count($vouchers) > 0)
<div class="mt-3">
<h6 class="mb-2">
{{ __('checkout.available_vouchers') }}
</h6>
<div class="d-flex flex-wrap gap-2">
@foreach($vouchers as $voucher)
<span class="badge bg-light text-dark px-3 py-2">
{{ $voucher->code }}
</span>
@endforeach
</div>
</div>
@endif
{{-- List Voucher Events --}}
@if($voucher_events && count($voucher_events) > 0)
<div class="mt-3">
<h6 class="mb-2">
{{ __('checkout.available_voucher_events') }}
</h6>
<div class="d-flex flex-wrap gap-2">
@foreach($voucher_events as $voucher_event)
<span class="badge bg-primary text-white px-3 py-2"
style="cursor:pointer"
data-id="{{ $voucher_event->id }}"
data-name="{{ $voucher_event->name }}"
data-description="{{ $voucher_event->description }}"
data-point="{{ $voucher_event->redeem_point }}"
data-start="{{ $voucher_event->start_date }}"
data-end="{{ $voucher_event->end_date }}"
data-terms="{{ $voucher_event->terms_and_conditions }}"
data-bs-toggle="modal"
data-bs-target="#voucherEventModal">
{{ $voucher_event->name ?? '-' }}
</span>
@endforeach
</div>
</div>
@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 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!');
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('voucherEventModal'));
modal.hide();
// Reload page to update cart
setTimeout(() => {
window.location.reload();
}, 1000);
} 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();
});
});
});
</script>

View File

@ -312,7 +312,7 @@
<!-- Account and Wishlist buttons visible on screens < 768px wide (md breakpoint) --> <!-- Account and Wishlist buttons visible on screens < 768px wide (md breakpoint) -->
<div class="offcanvas-header border-top px-0 py-3 mt-3 d-md-none"> <div class="offcanvas-header border-top px-0 py-3 mt-3 d-md-none">
<div class="nav nav-justified w-100"> <div class="nav nav-justified w-100">
<a class="nav-link border-end" href="{{ route('login') }}"> <a class="nav-link border-end" href="{{ route('profile') }}">
<i class="ci-user fs-lg opacity-60 me-2"></i> <i class="ci-user fs-lg opacity-60 me-2"></i>
{{ __('header.account') }} {{ __('header.account') }}
</a> </a>

View File

@ -5,6 +5,7 @@
@include('layouts.partials/title-meta') @include('layouts.partials/title-meta')
@vite(['resources/js/theme-switcher.js']) @vite(['resources/js/theme-switcher.js'])
@include('layouts.partials/head-css') @include('layouts.partials/head-css')
</head> </head>

View File

@ -1,5 +1,4 @@
@vite(['node_modules/choices.js/public/assets/styles/choices.min.css', 'node_modules/swiper/swiper-bundle.min.css', 'node_modules/glightbox/dist/css/glightbox.min.css', 'node_modules/simplebar/dist/simplebar.min.css', 'node_modules/flatpickr/dist/flatpickr.min.css', 'node_modules/nouislider/dist/nouislider.min.css', 'node_modules/img-comparison-slider/dist/styles.css']) @vite(['node_modules/choices.js/public/assets/styles/choices.min.css', 'node_modules/swiper/swiper-bundle.min.css', 'node_modules/glightbox/dist/css/glightbox.min.css', 'node_modules/simplebar/dist/simplebar.min.css', 'node_modules/flatpickr/dist/flatpickr.min.css', 'node_modules/nouislider/dist/nouislider.min.css', 'node_modules/img-comparison-slider/dist/styles.css', 'node_modules/bootstrap/dist/css/bootstrap.min.css'])
@vite(['resources/icons/cartzilla-icons.min.css']) @vite(['resources/icons/cartzilla-icons.min.css'])
@vite(['resources/scss/theme.scss']) @vite(['resources/scss/theme.scss'])

View File

@ -17,6 +17,7 @@ use App\Http\Controllers\RoutingController;
use App\Http\Controllers\SearchController; use App\Http\Controllers\SearchController;
use App\Http\Controllers\TncController; use App\Http\Controllers\TncController;
use App\Http\Controllers\HelpController; use App\Http\Controllers\HelpController;
use App\Http\Controllers\VoucherEventController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::group(['prefix' => '/dummy'], function () { Route::group(['prefix' => '/dummy'], function () {
@ -111,6 +112,10 @@ Route::middleware(['auth'])->prefix('/checkout')->group(function () {
}); });
Route::middleware(['auth'])->prefix('/voucher-events')->group(function () {
Route::post('/{voucherEvent}/redeem', [VoucherEventController::class, 'redeem'])->name('voucher-events.redeem');
});
Route::middleware(['auth'])->prefix('/orders')->group(function () { Route::middleware(['auth'])->prefix('/orders')->group(function () {
Route::get('/', [OrderController::class, 'index'])->name('orders'); Route::get('/', [OrderController::class, 'index'])->name('orders');