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:
2026-01-21 23:13:07 +01:00
parent e88423e882
commit 024733bb31
8 changed files with 872 additions and 29 deletions

View File

@@ -579,3 +579,144 @@
#license-search-input { #license-search-input {
width: 280px; 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;
}

View File

@@ -234,4 +234,266 @@
// Initialize when document is ready // Initialize when document is ready
$(document).ready(init); $(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); })(jQuery);

View File

@@ -3,7 +3,7 @@
# This file is distributed under the GPL-2.0-or-later. # This file is distributed under the GPL-2.0-or-later.
msgid "" msgid ""
msgstr "" 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" "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" "POT-Creation-Date: 2026-01-21T00:00:00+00:00\n"
"PO-Revision-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." msgid "Choose which format of email to send."
msgstr "Wählen Sie, welches E-Mail-Format gesendet werden soll." 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."

View File

@@ -2,7 +2,7 @@
# This file is distributed under the GPL-2.0-or-later. # This file is distributed under the GPL-2.0-or-later.
msgid "" msgid ""
msgstr "" 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" "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" "POT-Creation-Date: 2026-01-21T00:00:00+00:00\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -865,3 +865,77 @@ msgstr ""
msgid "Customer" msgid "Customer"
msgstr "" 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 ""

View File

@@ -55,6 +55,12 @@ final class AdminController
// AJAX handler for live search // AJAX handler for live search
add_action('wp_ajax_wclp_live_search', [$this, 'handleLiveSearch']); 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', [ wp_localize_script('wc-licensed-product-admin', 'wclpAdmin', [
'ajaxUrl' => admin_url('admin-ajax.php'), 'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('wclp_live_search'), 'nonce' => wp_create_nonce('wclp_live_search'),
'editNonce' => wp_create_nonce('wclp_inline_edit'),
'strings' => [ 'strings' => [
'noResults' => __('No licenses found', 'wc-licensed-product'), 'noResults' => __('No licenses found', 'wc-licensed-product'),
'searching' => __('Searching...', 'wc-licensed-product'), 'searching' => __('Searching...', 'wc-licensed-product'),
'error' => __('Search failed', '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]); 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) * Handle admin actions (update, delete licenses)
*/ */
@@ -1089,7 +1268,12 @@ final class AdminController
<th scope="row" class="check-column"> <th scope="row" class="check-column">
<input type="checkbox" name="license_ids[]" value="<?php echo esc_attr($item['license']->getId()); ?>"> <input type="checkbox" name="license_ids[]" value="<?php echo esc_attr($item['license']->getId()); ?>">
</th> </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> <td>
<?php if ($item['product_edit_url']): ?> <?php if ($item['product_edit_url']): ?>
<a href="<?php echo esc_url($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> <br><small><?php echo esc_html($item['customer_email']); ?></small>
<?php endif; ?> <?php endif; ?>
</td> </td>
<td><?php echo esc_html($item['license']->getDomain()); ?></td> <td class="wclp-editable-cell" data-field="domain" data-license-id="<?php echo esc_attr($item['license']->getId()); ?>">
<td> <span class="wclp-display-value"><?php echo esc_html($item['license']->getDomain()); ?></span>
<span class="license-status license-status-<?php echo esc_attr($item['license']->getStatus()); ?>"> <button type="button" class="wclp-edit-btn button-link" title="<?php esc_attr_e('Edit', 'wc-licensed-product'); ?>">
<?php echo esc_html(ucfirst($item['license']->getStatus())); ?> <span class="dashicons dashicons-edit"></span>
</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>
<td> <td class="wclp-editable-cell" data-field="status" data-license-id="<?php echo esc_attr($item['license']->getId()); ?>">
<?php <span class="wclp-display-value">
$expiresAt = $item['license']->getExpiresAt(); <span class="license-status license-status-<?php echo esc_attr($item['license']->getStatus()); ?>">
if ($expiresAt) { <?php echo esc_html(ucfirst($item['license']->getStatus())); ?>
echo esc_html($expiresAt->format(get_option('date_format'))); </span>
} else { </span>
echo '<span class="license-lifetime">' . esc_html__('Lifetime', 'wc-licensed-product') . '</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>
<td class="license-actions"> <td class="license-actions">
<div class="row-actions"> <div class="row-actions">

View File

@@ -424,6 +424,39 @@ class LicenseManager
return $result !== false; 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 * Update license domain
*/ */

View File

@@ -110,7 +110,12 @@
<th scope="row" class="check-column"> <th scope="row" class="check-column">
<input type="checkbox" name="license_ids[]" value="{{ item.license.id }}"> <input type="checkbox" name="license_ids[]" value="{{ item.license.id }}">
</th> </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> <td>
{% if item.product_edit_url %} {% if item.product_edit_url %}
<a href="{{ esc_url(item.product_edit_url) }}">{{ esc_html(item.product_name) }}</a> <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> <br><small>{{ esc_html(item.customer_email) }}</small>
{% endif %} {% endif %}
</td> </td>
<td>{{ esc_html(item.license.domain) }}</td> <td class="wclp-editable-cell" data-field="domain" data-license-id="{{ item.license.id }}">
<td> <span class="wclp-display-value">{{ esc_html(item.license.domain) }}</span>
<span class="license-status license-status-{{ item.license.status }}"> <button type="button" class="wclp-edit-btn button-link" title="{{ __('Edit') }}">
{{ item.license.status|capitalize }} <span class="dashicons dashicons-edit"></span>
</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>
<td> <td class="wclp-editable-cell" data-field="status" data-license-id="{{ item.license.id }}">
{% if item.license.expiresAt %} <span class="wclp-display-value">
{{ item.license.expiresAt|date('Y-m-d') }} <span class="license-status license-status-{{ item.license.status }}">
{% else %} {{ item.license.status|capitalize }}
<span class="license-lifetime">{{ __('Lifetime') }}</span> </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;">
<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>
<td class="license-actions"> <td class="license-actions">
<div class="row-actions"> <div class="row-actions">