463 lines
23 KiB
Plaintext
463 lines
23 KiB
Plaintext
@model Nop.Plugin.Misc.FruitBankPlugin.Models.CustomerCreditWidgetModel
|
|
@inject Nop.Services.Common.IGenericAttributeService genericAttributeService
|
|
@inject Nop.Core.IWorkContext workContext
|
|
|
|
@{
|
|
var remaining = Model.RemainingCredit;
|
|
var statusClass = !Model.HasCreditLimit ? "text-muted"
|
|
: remaining <= 0 ? "text-danger"
|
|
: remaining < Model.CreditLimit * 0.2m ? "text-warning"
|
|
: "text-success";
|
|
|
|
// Per-admin collapse state, persisted like the core nopCommerce cards
|
|
var admin = await workContext.GetCurrentCustomerAsync();
|
|
|
|
const string hideAttributesCardName = "FruitBank.CustomerAttributesCard.Hide";
|
|
var hideAttributesCard = await genericAttributeService.GetAttributeAsync<bool>(admin, hideAttributesCardName);
|
|
|
|
const string hideCreditCardName = "FruitBank.CustomerCreditCard.Hide";
|
|
var hideCreditCard = await genericAttributeService.GetAttributeAsync<bool>(admin, hideCreditCardName);
|
|
}
|
|
|
|
@* FruitBank customer-top section: injected into the CustomerDetailsBlock widget zone
|
|
(which renders at the BOTTOM of the customer page) and relocated to the top of
|
|
#customer-cards by the script below. Future non-credit elements go inside this
|
|
same wrapper so the single relocation keeps them all together at the top. *@
|
|
<div id="fruitbank-customer-top">
|
|
|
|
@* ── Customer attributes (generic attributes block) ───────────────────────── *@
|
|
<div class="card card-primary card-outline @(hideAttributesCard ? "collapsed-card" : "")" id="fruitbank-customer-attributes-card">
|
|
<div class="card-header with-border">
|
|
<h3 class="card-title">
|
|
<i class="fas fa-id-card"></i>
|
|
@T("Plugins.Misc.FruitBankPlugin.CustomerAttributes.Title")
|
|
</h3>
|
|
<div class="card-tools float-right">
|
|
<button type="button" class="btn btn-tool" data-card-widget="collapse">
|
|
<i class="fas @(hideAttributesCard ? "fa-plus" : "fa-minus")"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="form-group row">
|
|
<div class="col-md-3">
|
|
<label>@T("Plugins.Misc.FruitBankPlugin.CustomerAttributes.IsEkaer")</label>
|
|
</div>
|
|
<div class="col-md-9">
|
|
<span class="form-control-plaintext">
|
|
@* No name attribute: this must NOT post with the core Customer Edit form;
|
|
it is saved via AJAX on change to CustomerAttributesController.SetEkaer. *@
|
|
<input type="checkbox" id="fruitbank-isekaer" data-customer-id="@Model.CustomerId" @(Model.IsEkaer ? "checked" : "") />
|
|
<span id="fruitbank-isekaer-status" class="ml-2"></span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group row">
|
|
<div class="col-md-3">
|
|
<label>@T("Plugins.Misc.FruitBankPlugin.CustomerSites.Title")</label>
|
|
</div>
|
|
<div class="col-md-9">
|
|
<span class="form-control-plaintext" id="fruitbank-sites-summary">
|
|
@if (Model.SiteCount > 0)
|
|
{
|
|
<text>@Model.SiteCount @T("Plugins.Misc.FruitBankPlugin.CustomerSites.CountWord")</text>
|
|
if (!string.IsNullOrWhiteSpace(Model.DefaultSiteDisplay))
|
|
{
|
|
<text> (@T("Plugins.Misc.FruitBankPlugin.CustomerSites.DefaultLabel"): @Model.DefaultSiteDisplay)</text>
|
|
}
|
|
}
|
|
else
|
|
{
|
|
<span class="text-muted">@T("Plugins.Misc.FruitBankPlugin.CustomerSites.None")</span>
|
|
}
|
|
</span>
|
|
<button type="button" class="btn btn-default mt-1" data-toggle="modal" data-target="#fruitbank-sites-modal">
|
|
<i class="fas fa-map-marker-alt"></i>
|
|
@T("Plugins.Misc.FruitBankPlugin.CustomerSites.Edit")
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group row">
|
|
<div class="col-md-3">
|
|
<label>@T("Plugins.Misc.FruitBankPlugin.CustomerPlates.Title")</label>
|
|
</div>
|
|
<div class="col-md-9">
|
|
<span class="form-control-plaintext" id="fruitbank-plates-summary">
|
|
@if (Model.PlateCount > 0)
|
|
{
|
|
<text>@Model.PlateCount @T("Plugins.Misc.FruitBankPlugin.CustomerPlates.CountWord")</text>
|
|
if (!string.IsNullOrWhiteSpace(Model.DefaultPlate))
|
|
{
|
|
<text> (@T("Plugins.Misc.FruitBankPlugin.CustomerSites.DefaultLabel"): @Model.DefaultPlate)</text>
|
|
}
|
|
}
|
|
else
|
|
{
|
|
<span class="text-muted">@T("Plugins.Misc.FruitBankPlugin.CustomerPlates.None")</span>
|
|
}
|
|
</span>
|
|
<button type="button" class="btn btn-default mt-1" data-toggle="modal" data-target="#fruitbank-plates-modal">
|
|
<i class="fas fa-car"></i>
|
|
@T("Plugins.Misc.FruitBankPlugin.CustomerPlates.Edit")
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@* ── Customer credit ──────────────────────────────────────────────────────── *@
|
|
<div class="card card-primary card-outline @(hideCreditCard ? "collapsed-card" : "")" id="fruitbank-customer-credit-card">
|
|
<div class="card-header with-border">
|
|
<h3 class="card-title">
|
|
<i class="fas fa-credit-card"></i>
|
|
@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.PageTitle")
|
|
</h3>
|
|
<div class="card-tools float-right">
|
|
<button type="button" class="btn btn-tool" data-card-widget="collapse">
|
|
<i class="fas @(hideCreditCard ? "fa-plus" : "fa-minus")"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
|
|
<div class="form-group row">
|
|
<div class="col-md-3">
|
|
<label>@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.CreditLimit")</label>
|
|
</div>
|
|
<div class="col-md-9">
|
|
<span class="form-control-plaintext">
|
|
@(Model.HasCreditLimit ? Model.CreditLimit.ToString("N0") + " Ft" : T("Plugins.Misc.FruitBankPlugin.CustomerCredit.Unlimited").Text)
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group row">
|
|
<div class="col-md-3">
|
|
<label>@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.OutstandingBalance")</label>
|
|
</div>
|
|
<div class="col-md-9">
|
|
<span class="form-control-plaintext">
|
|
@Model.OutstandingBalance.ToString("N0") Ft
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group row">
|
|
<div class="col-md-3">
|
|
<label>@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.RemainingCredit")</label>
|
|
</div>
|
|
<div class="col-md-9">
|
|
<span class="form-control-plaintext @statusClass">
|
|
<strong>
|
|
@if (!Model.HasCreditLimit)
|
|
{
|
|
@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.Unlimited")
|
|
}
|
|
else
|
|
{
|
|
@(remaining!.Value.ToString("N0"))
|
|
<span>Ft</span>
|
|
}
|
|
</strong>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
@if (!string.IsNullOrWhiteSpace(Model.Comment))
|
|
{
|
|
<div class="form-group row">
|
|
<div class="col-md-3">
|
|
<label>@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.Comment")</label>
|
|
</div>
|
|
<div class="col-md-9">
|
|
<span class="form-control-plaintext text-muted">@Model.Comment</span>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<div class="form-group row">
|
|
<div class="col-md-9 offset-md-3">
|
|
<a href="/Admin/CustomerCredit/Details/@Model.CustomerId" class="btn btn-default">
|
|
<i class="fas fa-edit"></i>
|
|
@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.EditTitle")
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
@* ── Sites edit modal ───────────────────────────────────────────────────────── *@
|
|
<div id="fruitbank-sites-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="fruitbank-sites-modal-title">
|
|
<div class="modal-dialog modal-lg" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h4 class="modal-title" id="fruitbank-sites-modal-title">@T("Plugins.Misc.FruitBankPlugin.CustomerSites.ModalTitle")</h4>
|
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
@if (!Model.SiteAddresses.Any())
|
|
{
|
|
<div class="alert alert-info mb-0">@T("Plugins.Misc.FruitBankPlugin.CustomerSites.NoAddresses")</div>
|
|
}
|
|
else
|
|
{
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:90px">@T("Plugins.Misc.FruitBankPlugin.CustomerSites.IsSiteLabel")</th>
|
|
<th style="width:120px">@T("Plugins.Misc.FruitBankPlugin.CustomerSites.DefaultLabel")</th>
|
|
<th>@T("Plugins.Misc.FruitBankPlugin.CustomerSites.AddressLabel")</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var a in Model.SiteAddresses)
|
|
{
|
|
<tr>
|
|
<td class="text-center">
|
|
<input type="checkbox" class="fb-site-check" data-address-id="@a.AddressId" @(a.IsSite ? "checked" : "") />
|
|
</td>
|
|
<td class="text-center">
|
|
<input type="radio" name="fb-site-default" class="fb-site-default" value="@a.AddressId" @(a.IsDefault ? "checked" : "") @(a.IsSite ? "" : "disabled") />
|
|
</td>
|
|
<td>@a.Display</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
}
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">@T("Admin.Common.Cancel")</button>
|
|
@if (Model.SiteAddresses.Any())
|
|
{
|
|
<button type="button" class="btn btn-primary" id="fruitbank-sites-save" data-customer-id="@Model.CustomerId">
|
|
@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.Save")
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@* ── License plates edit modal ─────────────────────────────────────────────── *@
|
|
<div id="fruitbank-plates-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="fruitbank-plates-modal-title">
|
|
<div class="modal-dialog" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h4 class="modal-title" id="fruitbank-plates-modal-title">@T("Plugins.Misc.FruitBankPlugin.CustomerPlates.ModalTitle")</h4>
|
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="input-group mb-2">
|
|
<input type="text" id="fb-plate-new" class="form-control" placeholder="pl. ABC-123" />
|
|
<div class="input-group-append">
|
|
<button type="button" id="fb-plate-add" class="btn btn-secondary">@T("Plugins.Misc.FruitBankPlugin.CustomerPlates.Add")</button>
|
|
</div>
|
|
</div>
|
|
<table class="table table-sm table-hover" id="fb-plate-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:120px" class="text-center">@T("Plugins.Misc.FruitBankPlugin.CustomerSites.DefaultLabel")</th>
|
|
<th>@T("Plugins.Misc.FruitBankPlugin.CustomerPlates.Title")</th>
|
|
<th style="width:50px"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="fb-plate-tbody">
|
|
@foreach (var p in Model.LicensePlates)
|
|
{
|
|
<tr>
|
|
<td class="text-center"><input type="radio" name="fb-plate-default" value="@p.Plate" @(p.IsDefault ? "checked" : "") /></td>
|
|
<td><span class="fb-plate-text">@p.Plate</span></td>
|
|
<td class="text-center"><button type="button" class="btn btn-sm btn-link text-danger fb-plate-remove" title="Törlés">×</button></td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">@T("Admin.Common.Cancel")</button>
|
|
<button type="button" class="btn btn-primary" id="fruitbank-plates-save" data-customer-id="@Model.CustomerId">
|
|
@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.Save")
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
// The widget is rendered into the CustomerDetailsBlock zone at the bottom of
|
|
// #customer-cards; move the whole FruitBank section to the top so it shows first.
|
|
function moveFruitBankTopSection() {
|
|
var section = document.getElementById('fruitbank-customer-top');
|
|
var cards = document.getElementById('customer-cards');
|
|
if (section && cards && cards.firstElementChild !== section) {
|
|
cards.prepend(section);
|
|
}
|
|
}
|
|
if (document.readyState !== 'loading') {
|
|
moveFruitBankTopSection();
|
|
} else {
|
|
document.addEventListener('DOMContentLoaded', moveFruitBankTopSection);
|
|
}
|
|
})();
|
|
|
|
$(function () {
|
|
// Persist collapse state per admin, same as the core nopCommerce cards.
|
|
// AdminLTE handles the visual collapse via data-card-widget="collapse";
|
|
// this only saves the new state (class is toggled after this handler runs).
|
|
function bindCollapsePersist(cardId, attributeName) {
|
|
$('#' + cardId).on('click', 'button[data-card-widget="collapse"]', function () {
|
|
var collapsed = !$('#' + cardId).hasClass('collapsed-card');
|
|
saveUserPreferences('@(Url.Action("SavePreference", "Preferences"))', attributeName, collapsed);
|
|
});
|
|
}
|
|
bindCollapsePersist('fruitbank-customer-attributes-card', '@hideAttributesCardName');
|
|
bindCollapsePersist('fruitbank-customer-credit-card', '@hideCreditCardName');
|
|
|
|
// isEkaer: save on toggle via AJAX (the widget can't post through the core
|
|
// Customer Edit action, which only binds CustomerModel).
|
|
$('#fruitbank-isekaer').on('change', function () {
|
|
var $cb = $(this);
|
|
var value = $cb.is(':checked');
|
|
var $status = $('#fruitbank-isekaer-status');
|
|
var token = $('input[name="__RequestVerificationToken"]').first().val();
|
|
|
|
$cb.prop('disabled', true);
|
|
$status.html('<i class="fas fa-spinner fa-spin text-muted"></i>');
|
|
|
|
$.ajax({
|
|
url: '@Url.Action("SetEkaer", "CustomerAttributes")',
|
|
type: 'POST',
|
|
data: { customerId: $cb.data('customer-id'), value: value, __RequestVerificationToken: token },
|
|
success: function (res) {
|
|
if (res && res.success) {
|
|
$status.html('<i class="fas fa-check text-success"></i>');
|
|
} else {
|
|
$status.html('<i class="fas fa-times text-danger"></i>');
|
|
$cb.prop('checked', !value);
|
|
}
|
|
},
|
|
error: function () {
|
|
$status.html('<i class="fas fa-times text-danger"></i>');
|
|
$cb.prop('checked', !value);
|
|
},
|
|
complete: function () {
|
|
$cb.prop('disabled', false);
|
|
setTimeout(function () { $status.empty(); }, 2000);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ── Sites edit modal ──────────────────────────────────────────────────
|
|
// The default radio is only valid for rows checked as a site.
|
|
$('#fruitbank-sites-modal').on('change', '.fb-site-check', function () {
|
|
var id = $(this).data('address-id');
|
|
var $radio = $('.fb-site-default[value="' + id + '"]');
|
|
if ($(this).is(':checked')) {
|
|
$radio.prop('disabled', false);
|
|
} else {
|
|
$radio.prop('disabled', true).prop('checked', false);
|
|
}
|
|
});
|
|
|
|
$('#fruitbank-sites-save').on('click', function () {
|
|
var $btn = $(this);
|
|
var ids = [];
|
|
$('#fruitbank-sites-modal .fb-site-check:checked').each(function () {
|
|
ids.push($(this).data('address-id'));
|
|
});
|
|
var def = $('#fruitbank-sites-modal .fb-site-default:checked').val() || 0;
|
|
var token = $('input[name="__RequestVerificationToken"]').first().val();
|
|
|
|
$btn.prop('disabled', true);
|
|
$.ajax({
|
|
url: '@Url.Action("SaveSites", "CustomerAttributes")',
|
|
type: 'POST',
|
|
traditional: true, // serialize siteAddressIds as repeated keys for int[] binding
|
|
data: {
|
|
customerId: $btn.data('customer-id'),
|
|
siteAddressIds: ids,
|
|
defaultAddressId: def,
|
|
__RequestVerificationToken: token
|
|
},
|
|
success: function (res) {
|
|
if (res && res.success) {
|
|
// Reload to refresh the summary line with the saved state.
|
|
location.reload();
|
|
} else {
|
|
alert(res && res.error ? res.error : 'Hiba a mentés során.');
|
|
$btn.prop('disabled', false);
|
|
}
|
|
},
|
|
error: function () {
|
|
alert('Hiba a mentés során.');
|
|
$btn.prop('disabled', false);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ── License plates edit modal ──────────────────────────────────────────
|
|
function fbNormalizePlate(v) { return (v || '').trim().toUpperCase(); }
|
|
|
|
function fbAddPlateRow(plate, isDefault) {
|
|
var $tbody = $('#fb-plate-tbody');
|
|
var exists = false;
|
|
$tbody.find('.fb-plate-text').each(function () { if ($(this).text() === plate) exists = true; });
|
|
if (exists) return;
|
|
var row = '<tr>' +
|
|
'<td class="text-center"><input type="radio" name="fb-plate-default" value="' + plate + '"' + (isDefault ? ' checked' : '') + ' /></td>' +
|
|
'<td><span class="fb-plate-text">' + plate + '</span></td>' +
|
|
'<td class="text-center"><button type="button" class="btn btn-sm btn-link text-danger fb-plate-remove" title="Törlés">×</button></td>' +
|
|
'</tr>';
|
|
$tbody.append(row);
|
|
}
|
|
|
|
$('#fb-plate-add').on('click', function () {
|
|
var plate = fbNormalizePlate($('#fb-plate-new').val());
|
|
if (!plate) return;
|
|
fbAddPlateRow(plate, false);
|
|
$('#fb-plate-new').val('').focus();
|
|
});
|
|
|
|
$('#fb-plate-new').on('keypress', function (e) {
|
|
if (e.which === 13) { e.preventDefault(); $('#fb-plate-add').trigger('click'); }
|
|
});
|
|
|
|
$('#fruitbank-plates-modal').on('click', '.fb-plate-remove', function () {
|
|
$(this).closest('tr').remove();
|
|
});
|
|
|
|
$('#fruitbank-plates-save').on('click', function () {
|
|
var $btn = $(this);
|
|
var plates = [];
|
|
$('#fb-plate-tbody .fb-plate-text').each(function () { plates.push($(this).text()); });
|
|
var def = $('#fruitbank-plates-modal input[name="fb-plate-default"]:checked').val() || '';
|
|
var token = $('input[name="__RequestVerificationToken"]').first().val();
|
|
|
|
$btn.prop('disabled', true);
|
|
$.ajax({
|
|
url: '@Url.Action("SaveLicensePlates", "CustomerAttributes")',
|
|
type: 'POST',
|
|
traditional: true, // serialize plates as repeated keys for string[] binding
|
|
data: {
|
|
customerId: $btn.data('customer-id'),
|
|
plates: plates,
|
|
defaultPlate: def,
|
|
__RequestVerificationToken: token
|
|
},
|
|
success: function (res) {
|
|
if (res && res.success) { location.reload(); }
|
|
else { alert(res && res.error ? res.error : 'Hiba a mentés során.'); $btn.prop('disabled', false); }
|
|
},
|
|
error: function () { alert('Hiba a mentés során.'); $btn.prop('disabled', false); }
|
|
});
|
|
});
|
|
});
|
|
</script>
|