Files
sx-fc/app/views/foodcourt/qrpay/init.html.erb
2025-07-04 16:37:24 +06:30

500 lines
18 KiB
Plaintext

<style>
.processing-indicator {
display: flex;
gap: 6px;
}
.processing-dot {
width: 10px;
height: 10px;
background: #2196F3;
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out;
}
.processing-dot:nth-child(2) {
animation-delay: 0.2s;
}
.processing-dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes bounce {
0%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-12px);
}
}
#fullpage-loading {
display: none;
position: fixed;
z-index: 9999;
top: 0; left: 0;
width: 100vw; height: 100vh;
background: rgba(0, 0, 0, 0.712);
align-items: center;
justify-content: center;
}
</style>
<div class="container-fluid h-100">
<% if !@print_settings.nil? %>
<% if @print_settings.precision.to_i > 0
precision = @print_settings.precision
else
precision = 0
end
#check delimiter
if @print_settings.delimiter
delimiter = ","
else
delimiter = ""
end
%>
<% end %>
<div id="fullpage-loading" style="display:none; position:fixed; z-index:9999; top:0; left:0; width:100vw; height:100vh; background:rgba(0,0,0,0.35); align-items:center; justify-content:center;">
<div>
<div style="display:flex; gap:10px; justify-content:center;">
<div class="processing-dot"></div>
<div class="processing-dot"></div>
<div class="processing-dot"></div>
</div>
<div style="color:white; text-align:center; margin-top:1rem; font-size:1.2rem;">Please Wait...</div>
</div>
</div>
<div class="row clearfix h-100">
<% if @error %>
<input type="hidden" name="error_message" id="error_message" value="<%= json_escape(@error.to_json) %>">
<% end %>
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-6 d-flex flex-column h-100">
<div class="card h-100" style="margin-bottom: 15px">
<div class="card-header m-l-5 m-r-5">
<div id="order-title">
<div class="row p-l-5 p-r-5">
<div class="col-lg-6 col-md-6 col-sm-6"><strong>Receipt No :</strong> <span id="receipt_no"><%=@sale_data.receipt_no rescue ' '%></span></div>
<div class="col-lg-6 col-md-6 col-sm-6 text-left"><strong>Receipt Date :</strong> <span id="receipt_date"><%=@sale_data.receipt_date.strftime("%d/%m/%Y-%I:%M %p") rescue '-' %></span></div>
</div>
<div class="row p-l-5 p-r-5">
<div class="col-lg-6 col-md-6 col-sm-6"><strong>Table No :</strong> <%=@table_no%></div>
<span class="hidden" id="dining"><%if !@dining.nil?%><%= @dining.id rescue "-" %><%end%></span>
<div class="col-lg-6 col-md-6 col-sm-6 text-left"><strong>Sale ID :</strong> <span id="sale_id"><% if @sale_data %><%=@sale_data.sale_id %><% end %></span></div>
</div>
<div class="row p-l-5 p-r-5">
<div class="col-lg-6 col-md-6 col-sm-6">
<strong>Customer :</strong>
<% if @cashier_type == 'quick_service' || @cashier_type == 'food_court' %>
<button type="button" class="btn bg-info waves-effect" id='customer_name' data-toggle="modal" data-target="#read_modal"><%= @sale_data.customer.name%></button>
<input type="hidden" name="paypar_account_no" id="paypar_account_no" value='<%=@sale_data.customer.paypar_account_no%>' />
<% else %>
<input type="hidden" name="paypar_account_no" id="paypar_account_no" value='<%=@sale_data.customer.paypar_account_no%>' />
<span id="customer_name"><%= @sale_data.customer.name%></span>
<% end %>
<span class="hidden" id="membership_id"><%= @sale_data.customer.membership_id%></span>
<span class="hidden" id="member_discount"><%= @member_discount%></span></div>
<div class="col-lg-6 col-md-6 col-sm-6 text-left"><strong>Checkin Time : </strong> <%if !@checkin_time.nil?%><%= @checkin_time.strftime("%I:%M %p") %>
<%end%></div>
</div>
</div>
</div>
<div class="card-block m-l-5 m-r-5 m-t--10 d-flex flex-column h-100">
<div class="card-title">
<div class="card-text">
<table class="table" id="append-table">
<tr>
<!-- <tr> -->
<th>#</th>
<th class="item-name">Items</th>
<th class="item-attr">QTY</th>
<th class="item-attr">Price</th>
<!-- </tr> -->
</tr>
</table>
</div>
</div>
<div id="foodcourt-slimscroll" class="h-100">
<!-- <div id="table-details" class="card-text" style="min-height:400px; max-height:400px; overflow-x:scroll"> -->
<div id="table-details" class="card-text m-t--10" >
<table class="table summary-items" id="append-table">
<tbody>
<% sub_total = 0
count = 0
%>
<% @sale_data.sale_items.each do |sale_item|
count += 1
%>
<% sub_total += sale_item.price%>
<tr>
<td><%= count %></td>
<td class="item-name"><%=sale_item.product_name%>@<%=number_with_precision( sale_item.unit_price, precision: precision.to_i )%></td>
<td class="item-attr"><%=sale_item.qty%></td>
<td class="item-attr"><%=(number_with_precision(sale_item.price, precision: precision.to_i ))%></td>
</tr>
<%end %>
</tbody>
</table>
</div>
</div>
</div>
<div class="card-footer ">
<table class="table m-t--10 ">
<tfooter>
<tr>
<td class="charges-name"><strong>Sub Total</strong></td>
<td class="item-attr"><strong><span id="sub-total"><%=number_with_precision(sub_total, precision: precision.to_i)%></span></strong></td>
</tr>
<tr>
<%if @sale_data.discount_type == 'member_discount'%>
<td class="charges-name"><strong>Member Discount:</strong></td>
<%else%>
<td class="charges-name"><strong>(Discount)</strong></td>
<%end%>
<td class="item-attr"><strong><span>(<%= number_with_precision(@sale_data.total_discount, precision: precision.to_i ) rescue number_with_precision(0, precision: precision.to_i ) %>)</span></strong></td>
</tr>
<tr>
<td class="charges-name">
<strong>
<% if !@account_arr.empty? %>
Tax
(<% @i = 0
@account_arr.each do |ct| %>
<%=ct.tax_name%>
<% if @account_arr.count != @i+1%>
+ <% @i =+1 %>
<%end%>
<%end %>)
<% else %>
No Tax
<% end %></strong><br>
<%if @changable_tax %>
<% if @current_user.role == 'cashier' %>
<button class="btn btn-link waves-effect bg-info access_modal" data-type = 'change_tax' >Change Tax</button>
<% else %>
<button class="btn btn-link waves-effect bg-info change_tax">Change Tax</button>
<% end %>
<% end %>
</td>
<td class="item-attr"><strong><span id="total_tax"><%= number_with_precision(@sale_data.total_tax, precision: precision.to_i ) rescue number_with_precision(0, precision: precision.to_i )%></span></strong></td>
</tr>
<tr>
<td class="charges-name"><strong>Rounding Adj:</strong></td>
<td class="item-attr"><strong><%= number_with_precision(@sale_data.rounding_adjustment, precision: precision.to_i ) rescue number_with_precision(0, precision: precision.to_i )%></strong></td>
</tr>
<tr>
<td class="charges-name"><strong>Grand Total</strong></td>
<td class="item-attr"><strong><span><%= number_with_precision(@sale_data.grand_total, precision: precision.to_i ) rescue number_with_precision(0, precision: precision.to_i )%></span></strong></td>
</tr>
<%if @balance > 0%>
<tr>
<td class="charges-name"><strong><%= @accountable_type %></strong></td>
<td class="item-attr"><strong><span><%=number_with_precision(@balance, precision: precision.to_i )%></span></strong></td>
</tr>
<% end %>
<% if !@individual_total[0].nil? %>
<tr>
<td class="charges-name">
<strong>Split Bill for <%= @individual_total[0]['total_customer'] %> persons</strong>
</td>
<td></td>
</tr>
<tr>
<td class="charges-name">
<strong>Amount Due (per person)</strong>
</td>
<td class="item-attr"><strong><span><%= number_with_precision(@individual_total[0]['per_person_amount'], precision: precision.to_i )%></span></strong></td>
</tr>
<% end %>
</tfooter>
</table>
</div>
</div>
</div>
<div class="col-lg-5 col-md-5 col-sm-5 col-xs-5 d-flex flex-column h-100" style="padding-right: 10px;">
<div class="card-block">
<div class="payment-waiting text-center" style="min-height: 650px; display: flex; flex-direction: column; justify-content: space-between; align-items: center;">
<img src="/image/mmqr.webp" alt="MMQR Payment" style="max-width: 120px; margin-bottom: 10px;">
<div class="processing-indicator" style="margin: 1rem 0;">
<div class="processing-dot"></div>
<div class="processing-dot"></div>
<div class="processing-dot"></div>
</div>
<h3 class="m-t-20" style="color: #555;">Waiting for Customer Payment</h3>
<p class="text-muted">Please ask customer to scan the QR code</p>
<% if @qr_svg %>
<div>
<%= raw @qr_svg %>
</div>
<% end %>
<div class="d-flex" style="margin: 1.5rem 0;">
<button class="btn btn-danger" id="cancel-btn">Cancel Payment</button>
</div>
</div>
<input type="hidden" name="server_mode" value="<%=ENV["SERVER_MODE"]%>" id="server_mode">
<input type="hidden" name="display_type" id="display_type" value="<%= @display_type%>">
</div>
</div>
</div>
<input type="hidden" id="server_mode" value="<%= ENV["SERVER_MODE"] %>">
</div>
<div class="modal fade" id="voidModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="voidModalLabel">Please Enter Reason for Void</h4>
</div>
<div class="modal-body">
<input type="textarea" name="remark" class="form-control col-md-12 remark" id="remark">
</div>
<div class="modal-footer ">
<div class="row p-r-20">
<div class="col-md-5">
<button type="button" class="btn btn-link p-t-5 p-b-5 bg-red waves-effect " id="void" active="true">VOID</button>
</div>
<div class="col-md-5">
<button type="button" class="btn btn-link p-t-5 p-b-5 bg-blue waves-effect" data-dismiss="modal">CLOSE</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script defer type="text/javascript">
// disable actions while waiting for payment
$(document).ready(function() {
$('button, a').not('#cancel-btn').each(function() {
const $element = $(this);
if ($element.is('button')) {
$element.prop('disabled', true);
}
// Disable links and dropdowns
$element.addClass('disabled');
$element.css({
'pointer-events': 'none',
'opacity': '0.5',
'cursor': 'not-allowed'
});
// Prevent click actions
$element.on('click', function(e) {
e.preventDefault();
e.stopImmediatePropagation();
});
});
// Specifically disable Bootstrap dropdowns
$('.dropdown-toggle').not('#cancel-btn').each(function() {
const $dropdown = $(this);
// Remove Bootstrap functionality
$dropdown.removeAttr('data-bs-toggle');
$dropdown.off('click.bs.dropdown');
// Add visual
$dropdown.addClass('disabled');
$dropdown.css({
'pointer-events': 'none',
'opacity': '0.5',
'cursor': 'not-allowed'
});
});
});
// payment success
$(document).ready(function() {
const $paymentWaiting = $('.payment-waiting');
const amountToReceive = <%= number_with_precision(@sale_data.grand_total, precision: precision.to_i) %>;
const receiptNo = $('#receipt_no').text();
let paymentProcessed = false;
let fallbackTimeout;
let connected = false;
function handlePaymentSuccess() {
if (paymentProcessed) return;
paymentProcessed = true;
clearTimeout(fallbackTimeout);
$('#cancel-btn').hide();
$paymentWaiting.html(`
<img src="/image/mmqr.webp" alt="MMQR Payment" style="max-width: 120px; margin-bottom: 10px;">
<div class="payment-success text-center">
<i class="material-icons" style="font-size: 50px; color: #4CAF50;">check_circle</i>
<h3 class="m-t-20" style="color: #4CAF50;">Payment Successful!</h3>
<p>Amount Received: ${amountToReceive}</p>
</div>
`);
$.ajax({
url: '/foodcourt/qrpay/process_payment',
method: 'POST',
contentType: 'application/json',
headers: {
'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
},
data: JSON.stringify({ sale_id: "<%= @sale_data.sale_id %>" }),
success: (data) => {
if (data.status) {
customer_display_view({
msg: "Payment Successful",
paid_amount: amountToReceive,
receipt_no: receiptNo
}, "pay_success");
}
},
error: (xhr, status, error) => {
console.log("Error:", error);
}
});
setTimeout(() => {
window.location.href = "/";
}, 1500);
}
function checkPaymentStatus() {
$.ajax({
url: '/foodcourt/qrpay/check_payment_status',
method: 'POST',
contentType: 'application/json',
headers: {
'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
},
data: JSON.stringify({ receipt_no: receiptNo }),
success: function(response) {
if (response.status === "PAY_SUCCESS") {
handlePaymentSuccess();
} else if (response.status === "CONFIG_ERROR") {
clearTimeout(fallbackTimeout);
} else if (!paymentProcessed) {
fallbackTimeout = setTimeout(checkPaymentStatus, 5000);
}
},
error: function() {
if (!paymentProcessed) {
fallbackTimeout = setTimeout(checkPaymentStatus, 5000);
}
}
});
}
// ----- WebSocket Setup -----
const cable = ActionCable.createConsumer("wss://juicecorner-0mo.sx-fc.app/cable");
const subscription = cable.subscriptions.create(
{
channel: "NagatoChannel",
receipt_no: receiptNo
},
{
connected() {
console.log("Nagato channel connected");
connected = true;
fallbackTimeout = setTimeout(checkPaymentStatus, 30000);
},
received(data) {
console.log("Received:", data);
if (data.status === "PAY_SUCCESS" && !paymentProcessed) {
clearTimeout(fallbackTimeout);
handlePaymentSuccess();
}
},
disconnected() {
console.log("Nagato channel disconnected");
if (!paymentProcessed) {
fallbackTimeout = setTimeout(checkPaymentStatus, 30000);
}
}
}
);
// Fallback if WebSocket doesn't connect within 5 seconds
setTimeout(() => {
if (!connected) {
console.warn("WebSocket failed to connect — falling back to polling.");
checkPaymentStatus(); // start polling
}
}, 5000);
function customer_display_view(data, status) {
$.post('/foodcourt/customer_view', {
data: data,
status: status
}, function(result) {}, 'json');
}
$('#cancel-btn').on('click', function(e) {
e.preventDefault(); // Prevent any default button action
swal({
title: "Are you sure?",
text: "Do you really want to cancel this transaction?",
type: "warning",
showCancelButton: true,
confirmButtonColor: "#DD6B55",
confirmButtonText: "Yes, cancel it!",
cancelButtonText: "No, keep it.",
closeOnConfirm: false,
showLoaderOnConfirm: true,
},
function(isConfirm){
if (isConfirm) {
// User clicked "Yes", so we proceed with the cancellation.
const postData = {
sale_id: "<%= @sale_data.sale_id %>"
};
$.ajax({
url: '/foodcourt/qrpay/cancel',
method: 'POST',
contentType: 'application/json',
headers: {
'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
},
data: JSON.stringify(postData),
success: function(data) {
if (data.status) {
swal({
title: "Cancelled!",
text: "The transaction has been successfully cancelled. You will be redirected shortly.",
type: "success",
timer: 2000, // The alert will close automatically after 2 seconds
showConfirmButton: false
});
setTimeout(function() {
window.location.href = "/";
}, 1500); // 1.5-second delay before redirecting
} else {
swal("Error", "Could not cancel the transaction. Please try again.", "error");
}
},
error: function(xhr, status, error) {
console.log("Error:", error);
swal("AJAX Error", "An unexpected error occurred. Please check the console.", "error");
}
});
}
});
});
});
</script>