You've already forked wc-licensed-product
Add inline editing for licenses and copy license key button
- Add inline editing for status, expiry date, and domain fields - Add copy-to-clipboard button for license keys - Add AJAX handlers for inline editing with nonce verification - Update LicenseManager with updateLicenseExpiry method - Add new translations for inline editing strings (de_CH) - Compile updated German translations to .mo file Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -579,3 +579,144 @@
|
||||
#license-search-input {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
/* Inline Editing Styles */
|
||||
.wclp-editable-cell {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wclp-editable-cell .wclp-display-value {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.wclp-editable-cell .wclp-edit-btn {
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
color: #2271b1;
|
||||
padding: 0;
|
||||
margin-left: 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.wclp-editable-cell:hover .wclp-edit-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.wclp-editable-cell .wclp-edit-btn .dashicons {
|
||||
font-size: 14px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.wclp-editable-cell .wclp-edit-btn:hover {
|
||||
color: #135e96;
|
||||
}
|
||||
|
||||
.wclp-edit-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.wclp-edit-form .wclp-edit-input {
|
||||
max-width: 150px;
|
||||
height: 28px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.wclp-edit-form .wclp-edit-input[type="date"] {
|
||||
max-width: 130px;
|
||||
}
|
||||
|
||||
.wclp-edit-form select.wclp-edit-input {
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.wclp-edit-form .button-small {
|
||||
height: 26px;
|
||||
line-height: 24px;
|
||||
padding: 0 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.wclp-edit-form .wclp-lifetime-btn {
|
||||
font-size: 16px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
/* Inline notice animation */
|
||||
.wclp-inline-notice {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* Make editable cells have a minimum height for consistency */
|
||||
.licenses-table .wclp-editable-cell {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
/* Domain column wider for edit form */
|
||||
.licenses-table td[data-field="domain"] {
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
/* Status column */
|
||||
.licenses-table td[data-field="status"] {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
/* Expiry column */
|
||||
.licenses-table td[data-field="expiry"] {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
/* Copy License Key Button */
|
||||
.wclp-copy-btn {
|
||||
color: #2271b1;
|
||||
padding: 0;
|
||||
margin-left: 5px;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wclp-copy-btn:hover {
|
||||
color: #135e96;
|
||||
}
|
||||
|
||||
.wclp-copy-btn .dashicons {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.wclp-copy-tooltip {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #1d2327;
|
||||
color: #fff;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
z-index: 100;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.wclp-copy-tooltip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 5px solid transparent;
|
||||
border-top-color: #1d2327;
|
||||
}
|
||||
|
||||
.wclp-license-key {
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
@@ -234,4 +234,266 @@
|
||||
// Initialize when document is ready
|
||||
$(document).ready(init);
|
||||
|
||||
// ========================================
|
||||
// Inline Editing Functionality
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Initialize inline editing
|
||||
*/
|
||||
function initInlineEditing() {
|
||||
// Edit button click
|
||||
$(document).on('click', '.wclp-edit-btn', function(e) {
|
||||
e.preventDefault();
|
||||
var $cell = $(this).closest('.wclp-editable-cell');
|
||||
showEditForm($cell);
|
||||
});
|
||||
|
||||
// Cancel button click
|
||||
$(document).on('click', '.wclp-cancel-btn', function(e) {
|
||||
e.preventDefault();
|
||||
var $cell = $(this).closest('.wclp-editable-cell');
|
||||
hideEditForm($cell);
|
||||
});
|
||||
|
||||
// Save button click
|
||||
$(document).on('click', '.wclp-save-btn', function(e) {
|
||||
e.preventDefault();
|
||||
var $cell = $(this).closest('.wclp-editable-cell');
|
||||
saveEdit($cell);
|
||||
});
|
||||
|
||||
// Lifetime button click (for expiry field)
|
||||
$(document).on('click', '.wclp-lifetime-btn', function(e) {
|
||||
e.preventDefault();
|
||||
var $cell = $(this).closest('.wclp-editable-cell');
|
||||
$cell.find('.wclp-edit-input').val('');
|
||||
saveEdit($cell);
|
||||
});
|
||||
|
||||
// Enter key saves, Escape cancels
|
||||
$(document).on('keydown', '.wclp-edit-input', function(e) {
|
||||
var $cell = $(this).closest('.wclp-editable-cell');
|
||||
if (e.keyCode === 13) { // Enter
|
||||
e.preventDefault();
|
||||
saveEdit($cell);
|
||||
} else if (e.keyCode === 27) { // Escape
|
||||
e.preventDefault();
|
||||
hideEditForm($cell);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show edit form for a cell
|
||||
*/
|
||||
function showEditForm($cell) {
|
||||
$cell.find('.wclp-display-value, .wclp-edit-btn').hide();
|
||||
$cell.find('.wclp-edit-form').show();
|
||||
$cell.find('.wclp-edit-input').focus().select();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide edit form for a cell
|
||||
*/
|
||||
function hideEditForm($cell) {
|
||||
$cell.find('.wclp-edit-form').hide();
|
||||
$cell.find('.wclp-display-value, .wclp-edit-btn').show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save edit via AJAX
|
||||
*/
|
||||
function saveEdit($cell) {
|
||||
var field = $cell.data('field');
|
||||
var licenseId = $cell.data('license-id');
|
||||
var $input = $cell.find('.wclp-edit-input');
|
||||
var value = $input.val();
|
||||
var $saveBtn = $cell.find('.wclp-save-btn');
|
||||
var originalText = $saveBtn.text();
|
||||
|
||||
// Determine action based on field
|
||||
var action;
|
||||
var data = {
|
||||
action: '',
|
||||
nonce: wclpAdmin.editNonce,
|
||||
license_id: licenseId
|
||||
};
|
||||
|
||||
switch (field) {
|
||||
case 'status':
|
||||
data.action = 'wclp_update_license_status';
|
||||
data.status = value;
|
||||
break;
|
||||
case 'expiry':
|
||||
data.action = 'wclp_update_license_expiry';
|
||||
data.expiry_date = value;
|
||||
break;
|
||||
case 'domain':
|
||||
data.action = 'wclp_update_license_domain';
|
||||
data.domain = value;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
// Show saving state
|
||||
$saveBtn.text(wclpAdmin.strings.saving).prop('disabled', true);
|
||||
$cell.find('.wclp-cancel-btn, .wclp-lifetime-btn').prop('disabled', true);
|
||||
|
||||
$.ajax({
|
||||
url: wclpAdmin.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: data,
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
// Update display value
|
||||
updateDisplayValue($cell, field, response.data);
|
||||
hideEditForm($cell);
|
||||
showNotice('success', response.data.message);
|
||||
} else {
|
||||
showNotice('error', response.data.message || wclpAdmin.strings.saveFailed);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
showNotice('error', wclpAdmin.strings.saveFailed);
|
||||
},
|
||||
complete: function() {
|
||||
$saveBtn.text(originalText).prop('disabled', false);
|
||||
$cell.find('.wclp-cancel-btn, .wclp-lifetime-btn').prop('disabled', false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the display value after successful save
|
||||
*/
|
||||
function updateDisplayValue($cell, field, data) {
|
||||
var $display = $cell.find('.wclp-display-value');
|
||||
|
||||
switch (field) {
|
||||
case 'status':
|
||||
var statusHtml = '<span class="license-status license-status-' + escapeHtml(data.status) + '">' +
|
||||
escapeHtml(data.status_label) + '</span>';
|
||||
$display.html(statusHtml);
|
||||
break;
|
||||
case 'expiry':
|
||||
if (data.expiry_date) {
|
||||
$display.html(escapeHtml(data.expiry_display));
|
||||
} else {
|
||||
$display.html('<span class="license-lifetime">' + wclpAdmin.strings.lifetime + '</span>');
|
||||
}
|
||||
// Update the input value
|
||||
$cell.find('.wclp-edit-input').val(data.expiry_date || '');
|
||||
break;
|
||||
case 'domain':
|
||||
$display.text(data.domain);
|
||||
// Update the input value
|
||||
$cell.find('.wclp-edit-input').val(data.domain);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a temporary notice
|
||||
*/
|
||||
function showNotice(type, message) {
|
||||
var $notice = $('<div class="notice notice-' + type + ' is-dismissible wclp-inline-notice"><p>' +
|
||||
escapeHtml(message) + '</p></div>');
|
||||
|
||||
// Insert at the top of the wrap
|
||||
$('.wrap h1').first().after($notice);
|
||||
|
||||
// Auto-dismiss after 3 seconds
|
||||
setTimeout(function() {
|
||||
$notice.fadeOut(300, function() {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Initialize inline editing when document is ready
|
||||
$(document).ready(initInlineEditing);
|
||||
|
||||
// ========================================
|
||||
// Copy License Key Functionality
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Initialize copy license key buttons
|
||||
*/
|
||||
function initCopyButtons() {
|
||||
$(document).on('click', '.wclp-copy-btn', function(e) {
|
||||
e.preventDefault();
|
||||
var $btn = $(this);
|
||||
var licenseKey = $btn.data('license-key');
|
||||
|
||||
copyToClipboard(licenseKey).then(function() {
|
||||
showCopyFeedback($btn, true);
|
||||
}).catch(function() {
|
||||
showCopyFeedback($btn, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy text to clipboard
|
||||
*/
|
||||
function copyToClipboard(text) {
|
||||
// Modern Clipboard API
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
return navigator.clipboard.writeText(text);
|
||||
}
|
||||
|
||||
// Fallback for older browsers
|
||||
return new Promise(function(resolve, reject) {
|
||||
var textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
|
||||
try {
|
||||
var successful = document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
if (successful) {
|
||||
resolve();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
} catch (err) {
|
||||
document.body.removeChild(textarea);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show copy feedback
|
||||
*/
|
||||
function showCopyFeedback($btn, success) {
|
||||
var $icon = $btn.find('.dashicons');
|
||||
var originalClass = 'dashicons-clipboard';
|
||||
var feedbackClass = success ? 'dashicons-yes' : 'dashicons-no';
|
||||
var feedbackColor = success ? '#00a32a' : '#d63638';
|
||||
|
||||
// Change icon temporarily
|
||||
$icon.removeClass(originalClass).addClass(feedbackClass).css('color', feedbackColor);
|
||||
|
||||
// Show tooltip
|
||||
var message = success ? wclpAdmin.strings.copied : wclpAdmin.strings.copyFailed;
|
||||
var $tooltip = $('<span class="wclp-copy-tooltip">' + escapeHtml(message) + '</span>');
|
||||
$btn.append($tooltip);
|
||||
|
||||
// Reset after delay
|
||||
setTimeout(function() {
|
||||
$icon.removeClass(feedbackClass).addClass(originalClass).css('color', '');
|
||||
$tooltip.remove();
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
// Initialize copy buttons when document is ready
|
||||
$(document).ready(initCopyButtons);
|
||||
|
||||
})(jQuery);
|
||||
|
||||
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
# This file is distributed under the GPL-2.0-or-later.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: WC Licensed Product 0.0.8\n"
|
||||
"Project-Id-Version: WC Licensed Product 0.0.10\n"
|
||||
"Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/issues\n"
|
||||
"POT-Creation-Date: 2026-01-21T00:00:00+00:00\n"
|
||||
"PO-Revision-Date: 2026-01-21T00:00:00+00:00\n"
|
||||
@@ -865,3 +865,77 @@ msgstr "E-Mail-Typ"
|
||||
|
||||
msgid "Choose which format of email to send."
|
||||
msgstr "Wählen Sie, welches E-Mail-Format gesendet werden soll."
|
||||
|
||||
#. Admin - Inline Editing
|
||||
msgid "Searching..."
|
||||
msgstr "Suche..."
|
||||
|
||||
msgid "No results found"
|
||||
msgstr "Keine Ergebnisse gefunden"
|
||||
|
||||
msgid "Saving..."
|
||||
msgstr "Speichere..."
|
||||
|
||||
msgid "Saved"
|
||||
msgstr "Gespeichert"
|
||||
|
||||
msgid "Save failed"
|
||||
msgstr "Speichern fehlgeschlagen"
|
||||
|
||||
msgid "Are you sure you want to revoke this license? This action cannot be undone."
|
||||
msgstr "Sind Sie sicher, dass Sie diese Lizenz widerrufen möchten? Diese Aktion kann nicht rückgängig gemacht werden."
|
||||
|
||||
msgid "Edit"
|
||||
msgstr "Bearbeiten"
|
||||
|
||||
msgid "Save"
|
||||
msgstr "Speichern"
|
||||
|
||||
msgid "Status updated successfully."
|
||||
msgstr "Status erfolgreich aktualisiert."
|
||||
|
||||
msgid "Expiry date updated successfully."
|
||||
msgstr "Ablaufdatum erfolgreich aktualisiert."
|
||||
|
||||
msgid "License set to lifetime."
|
||||
msgstr "Lizenz auf lebenslang gesetzt."
|
||||
|
||||
msgid "Domain updated successfully."
|
||||
msgstr "Domain erfolgreich aktualisiert."
|
||||
|
||||
msgid "Invalid license ID."
|
||||
msgstr "Ungültige Lizenz-ID."
|
||||
|
||||
msgid "Invalid status."
|
||||
msgstr "Ungültiger Status."
|
||||
|
||||
msgid "Invalid date format."
|
||||
msgstr "Ungültiges Datumsformat."
|
||||
|
||||
msgid "Domain cannot be empty."
|
||||
msgstr "Domain darf nicht leer sein."
|
||||
|
||||
msgid "Failed to update status."
|
||||
msgstr "Status konnte nicht aktualisiert werden."
|
||||
|
||||
msgid "Failed to update expiry date."
|
||||
msgstr "Ablaufdatum konnte nicht aktualisiert werden."
|
||||
|
||||
msgid "Failed to update domain."
|
||||
msgstr "Domain konnte nicht aktualisiert werden."
|
||||
|
||||
msgid "Failed to revoke license."
|
||||
msgstr "Lizenz konnte nicht widerrufen werden."
|
||||
|
||||
msgid "Leave empty for lifetime"
|
||||
msgstr "Leer lassen für lebenslang"
|
||||
|
||||
#. Admin - Order License Management
|
||||
msgid "Saved!"
|
||||
msgstr "Gespeichert!"
|
||||
|
||||
msgid "Error saving. Please try again."
|
||||
msgstr "Fehler beim Speichern. Bitte versuchen Sie es erneut."
|
||||
|
||||
msgid "Failed to update license domain."
|
||||
msgstr "Lizenz-Domain konnte nicht aktualisiert werden."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# This file is distributed under the GPL-2.0-or-later.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: WC Licensed Product 0.0.8\n"
|
||||
"Project-Id-Version: WC Licensed Product 0.0.10\n"
|
||||
"Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/issues\n"
|
||||
"POT-Creation-Date: 2026-01-21T00:00:00+00:00\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -865,3 +865,77 @@ msgstr ""
|
||||
|
||||
msgid "Customer"
|
||||
msgstr ""
|
||||
|
||||
#. Admin - Inline Editing
|
||||
msgid "Searching..."
|
||||
msgstr ""
|
||||
|
||||
msgid "No results found"
|
||||
msgstr ""
|
||||
|
||||
msgid "Saving..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Saved"
|
||||
msgstr ""
|
||||
|
||||
msgid "Save failed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to revoke this license? This action cannot be undone."
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
msgid "Status updated successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "Expiry date updated successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "License set to lifetime."
|
||||
msgstr ""
|
||||
|
||||
msgid "Domain updated successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "Invalid license ID."
|
||||
msgstr ""
|
||||
|
||||
msgid "Invalid status."
|
||||
msgstr ""
|
||||
|
||||
msgid "Invalid date format."
|
||||
msgstr ""
|
||||
|
||||
msgid "Domain cannot be empty."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to update status."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to update expiry date."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to update domain."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to revoke license."
|
||||
msgstr ""
|
||||
|
||||
msgid "Leave empty for lifetime"
|
||||
msgstr ""
|
||||
|
||||
#. Admin - Order License Management
|
||||
msgid "Saved!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Error saving. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to update license domain."
|
||||
msgstr ""
|
||||
|
||||
@@ -55,6 +55,12 @@ final class AdminController
|
||||
|
||||
// AJAX handler for live search
|
||||
add_action('wp_ajax_wclp_live_search', [$this, 'handleLiveSearch']);
|
||||
|
||||
// AJAX handlers for inline editing
|
||||
add_action('wp_ajax_wclp_update_license_status', [$this, 'handleAjaxStatusUpdate']);
|
||||
add_action('wp_ajax_wclp_update_license_expiry', [$this, 'handleAjaxExpiryUpdate']);
|
||||
add_action('wp_ajax_wclp_update_license_domain', [$this, 'handleAjaxDomainUpdate']);
|
||||
add_action('wp_ajax_wclp_revoke_license', [$this, 'handleAjaxRevoke']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,10 +131,27 @@ final class AdminController
|
||||
wp_localize_script('wc-licensed-product-admin', 'wclpAdmin', [
|
||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('wclp_live_search'),
|
||||
'editNonce' => wp_create_nonce('wclp_inline_edit'),
|
||||
'strings' => [
|
||||
'noResults' => __('No licenses found', 'wc-licensed-product'),
|
||||
'searching' => __('Searching...', 'wc-licensed-product'),
|
||||
'error' => __('Search failed', 'wc-licensed-product'),
|
||||
'saving' => __('Saving...', 'wc-licensed-product'),
|
||||
'saved' => __('Saved', 'wc-licensed-product'),
|
||||
'saveFailed' => __('Save failed', 'wc-licensed-product'),
|
||||
'confirmRevoke' => __('Are you sure you want to revoke this license? This action cannot be undone.', 'wc-licensed-product'),
|
||||
'edit' => __('Edit', 'wc-licensed-product'),
|
||||
'cancel' => __('Cancel', 'wc-licensed-product'),
|
||||
'save' => __('Save', 'wc-licensed-product'),
|
||||
'lifetime' => __('Lifetime', 'wc-licensed-product'),
|
||||
'copied' => __('Copied!', 'wc-licensed-product'),
|
||||
'copyFailed' => __('Copy failed', 'wc-licensed-product'),
|
||||
],
|
||||
'statuses' => [
|
||||
['value' => 'active', 'label' => __('Active', 'wc-licensed-product')],
|
||||
['value' => 'inactive', 'label' => __('Inactive', 'wc-licensed-product')],
|
||||
['value' => 'expired', 'label' => __('Expired', 'wc-licensed-product')],
|
||||
['value' => 'revoked', 'label' => __('Revoked', 'wc-licensed-product')],
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -174,6 +197,162 @@ final class AdminController
|
||||
wp_send_json_success(['results' => $results]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle AJAX status update
|
||||
*/
|
||||
public function handleAjaxStatusUpdate(): void
|
||||
{
|
||||
check_ajax_referer('wclp_inline_edit', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_woocommerce')) {
|
||||
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')], 403);
|
||||
}
|
||||
|
||||
$licenseId = isset($_POST['license_id']) ? absint($_POST['license_id']) : 0;
|
||||
$status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : '';
|
||||
|
||||
if (!$licenseId) {
|
||||
wp_send_json_error(['message' => __('Invalid license ID.', 'wc-licensed-product')]);
|
||||
}
|
||||
|
||||
$validStatuses = [License::STATUS_ACTIVE, License::STATUS_INACTIVE, License::STATUS_EXPIRED, License::STATUS_REVOKED];
|
||||
if (!in_array($status, $validStatuses, true)) {
|
||||
wp_send_json_error(['message' => __('Invalid status.', 'wc-licensed-product')]);
|
||||
}
|
||||
|
||||
$success = $this->licenseManager->updateLicenseStatus($licenseId, $status);
|
||||
|
||||
if ($success) {
|
||||
wp_send_json_success([
|
||||
'message' => __('Status updated successfully.', 'wc-licensed-product'),
|
||||
'status' => $status,
|
||||
'status_label' => ucfirst($status),
|
||||
]);
|
||||
} else {
|
||||
wp_send_json_error(['message' => __('Failed to update status.', 'wc-licensed-product')]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle AJAX expiry date update
|
||||
*/
|
||||
public function handleAjaxExpiryUpdate(): void
|
||||
{
|
||||
check_ajax_referer('wclp_inline_edit', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_woocommerce')) {
|
||||
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')], 403);
|
||||
}
|
||||
|
||||
$licenseId = isset($_POST['license_id']) ? absint($_POST['license_id']) : 0;
|
||||
$expiryDate = isset($_POST['expiry_date']) ? sanitize_text_field($_POST['expiry_date']) : '';
|
||||
|
||||
if (!$licenseId) {
|
||||
wp_send_json_error(['message' => __('Invalid license ID.', 'wc-licensed-product')]);
|
||||
}
|
||||
|
||||
// Handle "lifetime" option
|
||||
if (empty($expiryDate) || strtolower($expiryDate) === 'lifetime') {
|
||||
$success = $this->licenseManager->setLicenseLifetime($licenseId);
|
||||
if ($success) {
|
||||
wp_send_json_success([
|
||||
'message' => __('License set to lifetime.', 'wc-licensed-product'),
|
||||
'expiry_date' => '',
|
||||
'expiry_display' => __('Lifetime', 'wc-licensed-product'),
|
||||
]);
|
||||
} else {
|
||||
wp_send_json_error(['message' => __('Failed to update expiry date.', 'wc-licensed-product')]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate date format
|
||||
try {
|
||||
$date = new \DateTimeImmutable($expiryDate);
|
||||
$success = $this->licenseManager->updateLicenseExpiry($licenseId, $date);
|
||||
|
||||
if ($success) {
|
||||
wp_send_json_success([
|
||||
'message' => __('Expiry date updated successfully.', 'wc-licensed-product'),
|
||||
'expiry_date' => $date->format('Y-m-d'),
|
||||
'expiry_display' => $date->format(get_option('date_format')),
|
||||
]);
|
||||
} else {
|
||||
wp_send_json_error(['message' => __('Failed to update expiry date.', 'wc-licensed-product')]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
wp_send_json_error(['message' => __('Invalid date format.', 'wc-licensed-product')]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle AJAX domain update
|
||||
*/
|
||||
public function handleAjaxDomainUpdate(): void
|
||||
{
|
||||
check_ajax_referer('wclp_inline_edit', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_woocommerce')) {
|
||||
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')], 403);
|
||||
}
|
||||
|
||||
$licenseId = isset($_POST['license_id']) ? absint($_POST['license_id']) : 0;
|
||||
$domain = isset($_POST['domain']) ? sanitize_text_field($_POST['domain']) : '';
|
||||
|
||||
if (!$licenseId) {
|
||||
wp_send_json_error(['message' => __('Invalid license ID.', 'wc-licensed-product')]);
|
||||
}
|
||||
|
||||
if (empty($domain)) {
|
||||
wp_send_json_error(['message' => __('Domain cannot be empty.', 'wc-licensed-product')]);
|
||||
}
|
||||
|
||||
$success = $this->licenseManager->transferLicense($licenseId, $domain);
|
||||
|
||||
if ($success) {
|
||||
// Get the normalized domain from the license
|
||||
$license = $this->licenseManager->getLicenseById($licenseId);
|
||||
$normalizedDomain = $license ? $license->getDomain() : $domain;
|
||||
|
||||
wp_send_json_success([
|
||||
'message' => __('Domain updated successfully.', 'wc-licensed-product'),
|
||||
'domain' => $normalizedDomain,
|
||||
]);
|
||||
} else {
|
||||
wp_send_json_error(['message' => __('Failed to update domain.', 'wc-licensed-product')]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle AJAX revoke
|
||||
*/
|
||||
public function handleAjaxRevoke(): void
|
||||
{
|
||||
check_ajax_referer('wclp_inline_edit', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_woocommerce')) {
|
||||
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')], 403);
|
||||
}
|
||||
|
||||
$licenseId = isset($_POST['license_id']) ? absint($_POST['license_id']) : 0;
|
||||
|
||||
if (!$licenseId) {
|
||||
wp_send_json_error(['message' => __('Invalid license ID.', 'wc-licensed-product')]);
|
||||
}
|
||||
|
||||
$success = $this->licenseManager->updateLicenseStatus($licenseId, License::STATUS_REVOKED);
|
||||
|
||||
if ($success) {
|
||||
wp_send_json_success([
|
||||
'message' => __('License revoked successfully.', 'wc-licensed-product'),
|
||||
'status' => License::STATUS_REVOKED,
|
||||
'status_label' => ucfirst(License::STATUS_REVOKED),
|
||||
]);
|
||||
} else {
|
||||
wp_send_json_error(['message' => __('Failed to revoke license.', 'wc-licensed-product')]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle admin actions (update, delete licenses)
|
||||
*/
|
||||
@@ -1089,7 +1268,12 @@ final class AdminController
|
||||
<th scope="row" class="check-column">
|
||||
<input type="checkbox" name="license_ids[]" value="<?php echo esc_attr($item['license']->getId()); ?>">
|
||||
</th>
|
||||
<td><code><?php echo esc_html($item['license']->getLicenseKey()); ?></code></td>
|
||||
<td>
|
||||
<code class="wclp-license-key"><?php echo esc_html($item['license']->getLicenseKey()); ?></code>
|
||||
<button type="button" class="wclp-copy-btn button-link" data-license-key="<?php echo esc_attr($item['license']->getLicenseKey()); ?>" title="<?php esc_attr_e('Copy to clipboard', 'wc-licensed-product'); ?>">
|
||||
<span class="dashicons dashicons-clipboard"></span>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($item['product_edit_url']): ?>
|
||||
<a href="<?php echo esc_url($item['product_edit_url']); ?>">
|
||||
@@ -1105,21 +1289,55 @@ final class AdminController
|
||||
<br><small><?php echo esc_html($item['customer_email']); ?></small>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?php echo esc_html($item['license']->getDomain()); ?></td>
|
||||
<td>
|
||||
<span class="license-status license-status-<?php echo esc_attr($item['license']->getStatus()); ?>">
|
||||
<?php echo esc_html(ucfirst($item['license']->getStatus())); ?>
|
||||
</span>
|
||||
<td class="wclp-editable-cell" data-field="domain" data-license-id="<?php echo esc_attr($item['license']->getId()); ?>">
|
||||
<span class="wclp-display-value"><?php echo esc_html($item['license']->getDomain()); ?></span>
|
||||
<button type="button" class="wclp-edit-btn button-link" title="<?php esc_attr_e('Edit', 'wc-licensed-product'); ?>">
|
||||
<span class="dashicons dashicons-edit"></span>
|
||||
</button>
|
||||
<div class="wclp-edit-form" style="display:none;">
|
||||
<input type="text" class="wclp-edit-input" value="<?php echo esc_attr($item['license']->getDomain()); ?>">
|
||||
<button type="button" class="wclp-save-btn button button-small button-primary"><?php esc_html_e('Save', 'wc-licensed-product'); ?></button>
|
||||
<button type="button" class="wclp-cancel-btn button button-small"><?php esc_html_e('Cancel', 'wc-licensed-product'); ?></button>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
$expiresAt = $item['license']->getExpiresAt();
|
||||
if ($expiresAt) {
|
||||
echo esc_html($expiresAt->format(get_option('date_format')));
|
||||
} else {
|
||||
echo '<span class="license-lifetime">' . esc_html__('Lifetime', 'wc-licensed-product') . '</span>';
|
||||
}
|
||||
?>
|
||||
<td class="wclp-editable-cell" data-field="status" data-license-id="<?php echo esc_attr($item['license']->getId()); ?>">
|
||||
<span class="wclp-display-value">
|
||||
<span class="license-status license-status-<?php echo esc_attr($item['license']->getStatus()); ?>">
|
||||
<?php echo esc_html(ucfirst($item['license']->getStatus())); ?>
|
||||
</span>
|
||||
</span>
|
||||
<button type="button" class="wclp-edit-btn button-link" title="<?php esc_attr_e('Edit', 'wc-licensed-product'); ?>">
|
||||
<span class="dashicons dashicons-edit"></span>
|
||||
</button>
|
||||
<div class="wclp-edit-form" style="display:none;">
|
||||
<select class="wclp-edit-input">
|
||||
<option value="active" <?php selected($item['license']->getStatus(), 'active'); ?>><?php esc_html_e('Active', 'wc-licensed-product'); ?></option>
|
||||
<option value="inactive" <?php selected($item['license']->getStatus(), 'inactive'); ?>><?php esc_html_e('Inactive', 'wc-licensed-product'); ?></option>
|
||||
<option value="expired" <?php selected($item['license']->getStatus(), 'expired'); ?>><?php esc_html_e('Expired', 'wc-licensed-product'); ?></option>
|
||||
<option value="revoked" <?php selected($item['license']->getStatus(), 'revoked'); ?>><?php esc_html_e('Revoked', 'wc-licensed-product'); ?></option>
|
||||
</select>
|
||||
<button type="button" class="wclp-save-btn button button-small button-primary"><?php esc_html_e('Save', 'wc-licensed-product'); ?></button>
|
||||
<button type="button" class="wclp-cancel-btn button button-small"><?php esc_html_e('Cancel', 'wc-licensed-product'); ?></button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="wclp-editable-cell" data-field="expiry" data-license-id="<?php echo esc_attr($item['license']->getId()); ?>">
|
||||
<?php $expiresAt = $item['license']->getExpiresAt(); ?>
|
||||
<span class="wclp-display-value">
|
||||
<?php if ($expiresAt): ?>
|
||||
<?php echo esc_html($expiresAt->format(get_option('date_format'))); ?>
|
||||
<?php else: ?>
|
||||
<span class="license-lifetime"><?php esc_html_e('Lifetime', 'wc-licensed-product'); ?></span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<button type="button" class="wclp-edit-btn button-link" title="<?php esc_attr_e('Edit', 'wc-licensed-product'); ?>">
|
||||
<span class="dashicons dashicons-edit"></span>
|
||||
</button>
|
||||
<div class="wclp-edit-form" style="display:none;">
|
||||
<input type="date" class="wclp-edit-input" value="<?php echo $expiresAt ? esc_attr($expiresAt->format('Y-m-d')) : ''; ?>" placeholder="<?php esc_attr_e('Leave empty for lifetime', 'wc-licensed-product'); ?>">
|
||||
<button type="button" class="wclp-save-btn button button-small button-primary"><?php esc_html_e('Save', 'wc-licensed-product'); ?></button>
|
||||
<button type="button" class="wclp-cancel-btn button button-small"><?php esc_html_e('Cancel', 'wc-licensed-product'); ?></button>
|
||||
<button type="button" class="wclp-lifetime-btn button button-small" title="<?php esc_attr_e('Set to lifetime', 'wc-licensed-product'); ?>">∞</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="license-actions">
|
||||
<div class="row-actions">
|
||||
|
||||
@@ -424,6 +424,39 @@ class LicenseManager
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update license expiry date
|
||||
*
|
||||
* @param int $licenseId License ID
|
||||
* @param \DateTimeImmutable $expiresAt New expiry date
|
||||
* @return bool Success
|
||||
*/
|
||||
public function updateLicenseExpiry(int $licenseId, \DateTimeImmutable $expiresAt): bool
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$license = $this->getLicenseById($licenseId);
|
||||
if (!$license) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$result = $wpdb->update(
|
||||
$tableName,
|
||||
['expires_at' => $expiresAt->format('Y-m-d H:i:s')],
|
||||
['id' => $licenseId],
|
||||
['%s'],
|
||||
['%d']
|
||||
);
|
||||
|
||||
// If license was expired and new date is in the future, reactivate it
|
||||
if ($result !== false && $license->getStatus() === License::STATUS_EXPIRED && $expiresAt > new \DateTimeImmutable()) {
|
||||
$this->updateLicenseStatus($licenseId, License::STATUS_ACTIVE);
|
||||
}
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update license domain
|
||||
*/
|
||||
|
||||
@@ -110,7 +110,12 @@
|
||||
<th scope="row" class="check-column">
|
||||
<input type="checkbox" name="license_ids[]" value="{{ item.license.id }}">
|
||||
</th>
|
||||
<td><code>{{ item.license.licenseKey }}</code></td>
|
||||
<td>
|
||||
<code class="wclp-license-key">{{ item.license.licenseKey }}</code>
|
||||
<button type="button" class="wclp-copy-btn button-link" data-license-key="{{ esc_attr(item.license.licenseKey) }}" title="{{ __('Copy to clipboard') }}">
|
||||
<span class="dashicons dashicons-clipboard"></span>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
{% if item.product_edit_url %}
|
||||
<a href="{{ esc_url(item.product_edit_url) }}">{{ esc_html(item.product_name) }}</a>
|
||||
@@ -124,18 +129,54 @@
|
||||
<br><small>{{ esc_html(item.customer_email) }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ esc_html(item.license.domain) }}</td>
|
||||
<td>
|
||||
<span class="license-status license-status-{{ item.license.status }}">
|
||||
{{ item.license.status|capitalize }}
|
||||
</span>
|
||||
<td class="wclp-editable-cell" data-field="domain" data-license-id="{{ item.license.id }}">
|
||||
<span class="wclp-display-value">{{ esc_html(item.license.domain) }}</span>
|
||||
<button type="button" class="wclp-edit-btn button-link" title="{{ __('Edit') }}">
|
||||
<span class="dashicons dashicons-edit"></span>
|
||||
</button>
|
||||
<div class="wclp-edit-form" style="display:none;">
|
||||
<input type="text" class="wclp-edit-input" value="{{ esc_attr(item.license.domain) }}">
|
||||
<button type="button" class="wclp-save-btn button button-small button-primary">{{ __('Save') }}</button>
|
||||
<button type="button" class="wclp-cancel-btn button button-small">{{ __('Cancel') }}</button>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if item.license.expiresAt %}
|
||||
{{ item.license.expiresAt|date('Y-m-d') }}
|
||||
{% else %}
|
||||
<span class="license-lifetime">{{ __('Lifetime') }}</span>
|
||||
{% endif %}
|
||||
<td class="wclp-editable-cell" data-field="status" data-license-id="{{ item.license.id }}">
|
||||
<span class="wclp-display-value">
|
||||
<span class="license-status license-status-{{ item.license.status }}">
|
||||
{{ item.license.status|capitalize }}
|
||||
</span>
|
||||
</span>
|
||||
<button type="button" class="wclp-edit-btn button-link" title="{{ __('Edit') }}">
|
||||
<span class="dashicons dashicons-edit"></span>
|
||||
</button>
|
||||
<div class="wclp-edit-form" style="display:none;">
|
||||
<select class="wclp-edit-input">
|
||||
<option value="active" {{ item.license.status == 'active' ? 'selected' : '' }}>{{ __('Active') }}</option>
|
||||
<option value="inactive" {{ item.license.status == 'inactive' ? 'selected' : '' }}>{{ __('Inactive') }}</option>
|
||||
<option value="expired" {{ item.license.status == 'expired' ? 'selected' : '' }}>{{ __('Expired') }}</option>
|
||||
<option value="revoked" {{ item.license.status == 'revoked' ? 'selected' : '' }}>{{ __('Revoked') }}</option>
|
||||
</select>
|
||||
<button type="button" class="wclp-save-btn button button-small button-primary">{{ __('Save') }}</button>
|
||||
<button type="button" class="wclp-cancel-btn button button-small">{{ __('Cancel') }}</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="wclp-editable-cell" data-field="expiry" data-license-id="{{ item.license.id }}">
|
||||
<span class="wclp-display-value">
|
||||
{% if item.license.expiresAt %}
|
||||
{{ item.license.expiresAt|date('Y-m-d') }}
|
||||
{% else %}
|
||||
<span class="license-lifetime">{{ __('Lifetime') }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<button type="button" class="wclp-edit-btn button-link" title="{{ __('Edit') }}">
|
||||
<span class="dashicons dashicons-edit"></span>
|
||||
</button>
|
||||
<div class="wclp-edit-form" style="display:none;">
|
||||
<input type="date" class="wclp-edit-input" value="{{ item.license.expiresAt ? item.license.expiresAt|date('Y-m-d') : '' }}" placeholder="{{ __('Leave empty for lifetime') }}">
|
||||
<button type="button" class="wclp-save-btn button button-small button-primary">{{ __('Save') }}</button>
|
||||
<button type="button" class="wclp-cancel-btn button button-small">{{ __('Cancel') }}</button>
|
||||
<button type="button" class="wclp-lifetime-btn button button-small" title="{{ __('Set to lifetime') }}">∞</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="license-actions">
|
||||
<div class="row-actions">
|
||||
|
||||
Reference in New Issue
Block a user