478 lines
17 KiB
Plaintext
478 lines
17 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%><%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) {
|
|
document.getElementById('fullpage-loading').style.display = 'flex';
|
|
|
|
setTimeout(function() {
|
|
document.getElementById('fullpage-loading').style.display = 'none';
|
|
}, 5000);
|
|
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) {
|
|
customer_display_view(null, "reload");
|
|
}
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.log("Error:", error);
|
|
}
|
|
});
|
|
|
|
setTimeout(() => {
|
|
window.location.href = "/";
|
|
}, 1500);
|
|
});
|
|
});
|
|
|
|
</script>
|