ipdate
This commit is contained in:
Vendored
+10
-8
@@ -16,16 +16,18 @@ window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
* allows your team to easily build robust real-time web applications.
|
||||
*/
|
||||
|
||||
// import Echo from 'laravel-echo';
|
||||
|
||||
// window.Pusher = require('pusher-js');
|
||||
import Echo from "laravel-echo";
|
||||
|
||||
// window.Echo = new Echo({
|
||||
// broadcaster: 'pusher',
|
||||
// key: process.env.MIX_PUSHER_APP_KEY,
|
||||
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
|
||||
// forceTLS: true
|
||||
// });
|
||||
window.Pusher = require("pusher-js");
|
||||
|
||||
window.Echo = new Echo({
|
||||
broadcaster: "pusher",
|
||||
key: process.env.MIX_PUSHER_APP_KEY,
|
||||
cluster: process.env.MIX_PUSHER_APP_CLUSTER,
|
||||
forceTLS: true,
|
||||
encrypted: true,
|
||||
});
|
||||
|
||||
// import Echo from 'laravel-echo'
|
||||
// import socketio from 'socket.io-client'
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
@if(customer())
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 1. Check & Request Notification Permission
|
||||
function checkNotificationPermission() {
|
||||
if (!("Notification" in window)) {
|
||||
console.warn("This browser does not support desktop notification");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Notification.permission === "granted") {
|
||||
// Already granted
|
||||
} else if (Notification.permission !== "denied") {
|
||||
Notification.requestPermission().then(function (permission) {
|
||||
if (permission === "granted") {
|
||||
console.log("Notification permission granted!");
|
||||
new Notification("Thông báo hệ thống", {
|
||||
body: "Bạn sẽ nhận được thông báo khi các tác vụ hoàn tất.",
|
||||
icon: "{{ asset('images/favicon.png') }}"
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Call check on load
|
||||
checkNotificationPermission();
|
||||
|
||||
// 2. Listen for Private Channel Events
|
||||
if (window.Echo) {
|
||||
const userId = {{ customer()->id }};
|
||||
window.Echo.private('user.' + userId)
|
||||
.listen('.vm.status.changed', (e) => {
|
||||
console.log("VM Status Changed:", e);
|
||||
|
||||
// Show Browser Notification
|
||||
if (Notification.permission === "granted") {
|
||||
const notification = new Notification("Thông báo từ Portal", {
|
||||
body: e.message,
|
||||
icon: "{{ asset('images/favicon.png') }}", // Replace with actual logo path
|
||||
tag: 'vm-status-' + e.vmId
|
||||
});
|
||||
|
||||
notification.onclick = function() {
|
||||
window.focus();
|
||||
// Optional: navigating to VM detail page
|
||||
// window.location.href = '/service/' + e.vmId;
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
|
||||
// Show In-App Toast (using FuiToast based on your script.blade.php)
|
||||
if (typeof FuiToast !== 'undefined') {
|
||||
FuiToast.success(e.message);
|
||||
} else if (typeof toastr !== 'undefined') {
|
||||
toastr.success(e.message);
|
||||
}
|
||||
|
||||
// Optional: Reload Livewire components if needed
|
||||
if (window.Livewire) {
|
||||
// Livewire.dispatch('refreshComponent');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endif
|
||||
@@ -14,3 +14,5 @@
|
||||
@stack('viewScripts')
|
||||
<script src="{{ asset('js/main.min.js') }}"></script>
|
||||
@stack('consoleScripts')
|
||||
@include('frontend.layouts.partials.notification-script')
|
||||
|
||||
|
||||
@@ -1,26 +1,107 @@
|
||||
@props(['invoice'])
|
||||
|
||||
@php
|
||||
$initialPrice = (int) ($invoice->invoiceItem->first()?->getProductAttribute()?->price_sale ?? $invoice->invoiceItem->first()?->getProductAttribute()?->price ?? 0);
|
||||
@endphp
|
||||
|
||||
<flux:modal name="renewal-modal" class="md:w-[28rem]">
|
||||
<div class="space-y-6" x-data="{ submitting: false, plan: '12', months: 12, unitPrice: 0, total: 0 }" x-init="
|
||||
(() => {
|
||||
const p = {{ (int) ($invoice->invoiceItem->first()?->getProductAttribute()?->price_sale ?? $invoice->invoiceItem->first()?->getProductAttribute()?->price ?? 0) }};
|
||||
unitPrice = p; total = p * months;
|
||||
$watch('plan', value => { months = (value === 'other') ? months : parseInt(value); total = unitPrice * months; });
|
||||
$watch('months', value => { if (plan === 'other') { total = unitPrice * (parseInt(value) || 0); } });
|
||||
})()
|
||||
">
|
||||
<div class="space-y-6" x-data="{
|
||||
submitting: false,
|
||||
plan: '12',
|
||||
months: 12,
|
||||
unitPrice: {{ $initialPrice }},
|
||||
total: {{ $initialPrice * 12 }},
|
||||
loading: false,
|
||||
|
||||
updateTotal() {
|
||||
// Immediate client-side update for responsiveness
|
||||
let m = (this.plan === 'other') ? (parseInt(this.months) || 0) : parseInt(this.plan);
|
||||
if (m > 0) {
|
||||
this.total = this.unitPrice * m;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchPrice() {
|
||||
this.loading = true;
|
||||
try {
|
||||
// Determine months based on plan
|
||||
let m = (this.plan === 'other') ? (parseInt(this.months) || 1) : parseInt(this.plan);
|
||||
if (m < 2 && this.plan === 'other') m = 2;
|
||||
if (m < 1 && this.plan !== 'other') m = parseInt(this.plan);
|
||||
|
||||
// Optimistic UI update
|
||||
if (this.plan !== 'other') this.months = m;
|
||||
this.updateTotal();
|
||||
|
||||
const response = await fetch('{{ route('service.renew.price') }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
product_id: '{{ $invoice->vm_id }}',
|
||||
plan: this.plan,
|
||||
months: m
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.error) {
|
||||
this.unitPrice = data.data.unit_price;
|
||||
this.total = data.data.total;
|
||||
// Ensure months sync
|
||||
if (this.plan !== 'other') {
|
||||
this.months = m;
|
||||
}
|
||||
} else {
|
||||
console.error('Renew Price Error:', data.message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Fetch Error:', e);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
init() {
|
||||
// Fetch fresh price on load to be sure
|
||||
this.fetchPrice();
|
||||
|
||||
this.$watch('plan', (value) => {
|
||||
console.log('Plan changed:', value);
|
||||
this.fetchPrice();
|
||||
});
|
||||
|
||||
this.$watch('months', (value) => {
|
||||
if (this.plan === 'other') {
|
||||
if (value >= 2) {
|
||||
// Debounce slightly or just fetch
|
||||
this.fetchPrice();
|
||||
} else {
|
||||
this.updateTotal(); // Just update math locally
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Gia hạn dịch vụ') }}</flux:heading>
|
||||
<flux:text class="mt-2">{{ __('Chọn thời hạn gia hạn hoặc nhập số tháng mong muốn.') }}</flux:text>
|
||||
<flux:text class="mt-2">{{ __('Chọn thời hạn gia hạn hoặc nhập số tháng mong muốn (Tối thiểu 2 tháng).') }}
|
||||
</flux:text>
|
||||
</div>
|
||||
|
||||
<form x-on:submit.prevent="if (submitting) return; submitting = true; $el.submit()" method="POST" action="{{ route('service.renew') }}">
|
||||
|
||||
<form x-on:submit.prevent="if (submitting) return; submitting = true; $el.submit()" method="POST"
|
||||
action="{{ route('service.renew') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="product_id" value="{{ $invoice->vm_id }}">
|
||||
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Chọn thời hạn') }}</flux:label>
|
||||
<flux:select x-model="plan" name="plan">
|
||||
<flux:select x-model="plan" name="plan" x-on:change="fetchPrice()">
|
||||
{{-- <flux:select.option value="1">{{ __('1 tháng') }}</flux:select.option> --}}
|
||||
<!-- Min 2 months required -->
|
||||
<flux:select.option value="3">{{ __('3 tháng') }}</flux:select.option>
|
||||
<flux:select.option value="6">{{ __('6 tháng') }}</flux:select.option>
|
||||
<flux:select.option value="12">{{ __('12 tháng') }}</flux:select.option>
|
||||
@@ -28,25 +109,46 @@
|
||||
<flux:select.option value="other">{{ __('Khác...') }}</flux:select.option>
|
||||
</flux:select>
|
||||
</flux:field>
|
||||
|
||||
<div x-show="plan === 'other'" class="mt-2">
|
||||
|
||||
<div x-show="plan === 'other'" class="mt-2" x-transition>
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Số tháng') }}</flux:label>
|
||||
<flux:input x-model.number="months" name="months" type="number" min="1" max="60" placeholder="{{ __('Nhập số tháng') }}" />
|
||||
<flux:description>{{ __('Nhập số tháng muốn gia hạn (tối đa 60).') }}</flux:description>
|
||||
<flux:input x-model.number="months" name="months" type="number" min="2" max="60"
|
||||
placeholder="{{ __('Nhập số tháng') }}" />
|
||||
<flux:description>{{ __('Nhập số tháng muốn gia hạn (tối thiểu 2, tối đa 60).') }}</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-sm text-gray-700">
|
||||
<div class="flex justify-between"><span>{{ __('Đơn giá / tháng') }}</span><span x-text="new Intl.NumberFormat('vi-VN').format(unitPrice) + ' đ'"></span></div>
|
||||
<div class="flex justify-between font-semibold"><span>{{ __('Tổng tiền') }}</span><span x-text="new Intl.NumberFormat('vi-VN').format(total) + ' đ'"></span></div>
|
||||
|
||||
<div class="mt-4 p-4 bg-gray-50 rounded-lg border border-gray-200 text-sm text-gray-700 relative">
|
||||
<div x-show="loading" class="absolute inset-0 bg-white/50 flex items-center justify-center z-10 backdrop-blur-sm">
|
||||
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-orange-500"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-gray-500">{{ __('Đơn giá / tháng') }}</span>
|
||||
<span class="font-medium"
|
||||
x-text="new Intl.NumberFormat('vi-VN', { style: 'currency', currency: 'VND' }).format(unitPrice)"></span>
|
||||
</div>
|
||||
<div class="border-t border-dashed border-gray-300 my-2"></div>
|
||||
<div class="flex justify-between items-center text-base">
|
||||
<span class="font-semibold text-gray-800">{{ __('Tổng thanh toán') }}</span>
|
||||
<span class="font-bold text-orange-600 text-lg"
|
||||
x-text="new Intl.NumberFormat('vi-VN', { style: 'currency', currency: 'VND' }).format(total)"></span>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-gray-400 text-right italic">
|
||||
{{ __('(Đã bao gồm VAT nếu có)') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex gap-2 justify-end mt-6">
|
||||
<flux:modal.close>
|
||||
<flux:button variant="ghost">{{ __('Hủy') }}</flux:button>
|
||||
</flux:modal.close>
|
||||
<flux:button type="submit" variant="primary" color="orange" x-bind:disabled="submitting">{{ __('Đồng ý') }}</flux:button>
|
||||
<flux:button type="submit" variant="primary" color="orange"
|
||||
x-bind:disabled="submitting || loading || (plan === 'other' && months < 2)">
|
||||
<span x-show="!submitting">{{ __('Thanh toán gia hạn') }}</span>
|
||||
<span x-show="submitting">{{ __('Đang xử lý...') }}</span>
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user