feat: Add variation-level license duration to product editor
- Added license_duration_days field to ProductVariant type - Added License Duration input to each variation card - Backend: ProductsController saves/loads variation-level _license_duration_days meta - Allows different license periods per variation (e.g., 1-year, 2-year, lifetime)
This commit is contained in:
@@ -22,6 +22,7 @@ export type ProductVariant = {
|
|||||||
manage_stock?: boolean;
|
manage_stock?: boolean;
|
||||||
stock_status?: 'instock' | 'outofstock' | 'onbackorder';
|
stock_status?: 'instock' | 'outofstock' | 'onbackorder';
|
||||||
image?: string;
|
image?: string;
|
||||||
|
license_duration_days?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type VariationsTabProps = {
|
type VariationsTabProps = {
|
||||||
@@ -43,10 +44,10 @@ export function VariationsTab({
|
|||||||
}: VariationsTabProps) {
|
}: VariationsTabProps) {
|
||||||
const store = getStoreCurrency();
|
const store = getStoreCurrency();
|
||||||
const [copiedLink, setCopiedLink] = useState<string | null>(null);
|
const [copiedLink, setCopiedLink] = useState<string | null>(null);
|
||||||
|
|
||||||
const siteUrl = window.location.origin;
|
const siteUrl = window.location.origin;
|
||||||
const spaPagePath = '/store';
|
const spaPagePath = '/store';
|
||||||
|
|
||||||
const generateLink = (variationId: number, redirect: 'cart' | 'checkout' = 'cart') => {
|
const generateLink = (variationId: number, redirect: 'cart' | 'checkout' = 'cart') => {
|
||||||
if (!productId) return '';
|
if (!productId) return '';
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
@@ -55,7 +56,7 @@ export function VariationsTab({
|
|||||||
params.set('redirect', redirect);
|
params.set('redirect', redirect);
|
||||||
return `${siteUrl}${spaPagePath}?${params.toString()}`;
|
return `${siteUrl}${spaPagePath}?${params.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyToClipboard = async (link: string, label: string) => {
|
const copyToClipboard = async (link: string, label: string) => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(link);
|
await navigator.clipboard.writeText(link);
|
||||||
@@ -66,7 +67,7 @@ export function VariationsTab({
|
|||||||
toast.error('Failed to copy link');
|
toast.error('Failed to copy link');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addAttribute = () => {
|
const addAttribute = () => {
|
||||||
setAttributes([...attributes, { name: '', options: [], variation: false }]);
|
setAttributes([...attributes, { name: '', options: [], variation: false }]);
|
||||||
};
|
};
|
||||||
@@ -83,7 +84,7 @@ export function VariationsTab({
|
|||||||
|
|
||||||
const generateVariations = () => {
|
const generateVariations = () => {
|
||||||
const variationAttrs = attributes.filter(attr => attr.variation && attr.options.length > 0);
|
const variationAttrs = attributes.filter(attr => attr.variation && attr.options.length > 0);
|
||||||
|
|
||||||
if (variationAttrs.length === 0) {
|
if (variationAttrs.length === 0) {
|
||||||
toast.warning(__('Please add at least one variation attribute with options'));
|
toast.warning(__('Please add at least one variation attribute with options'));
|
||||||
return;
|
return;
|
||||||
@@ -276,6 +277,26 @@ export function VariationsTab({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* License Duration - only show if licensing is enabled on product */}
|
||||||
|
<div className="col-span-2 md:col-span-4">
|
||||||
|
<Label className="text-xs">{__('License Duration (Days)')}</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
placeholder={__('Leave empty to use product default')}
|
||||||
|
value={variation.license_duration_days || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updated = [...variations];
|
||||||
|
updated[index].license_duration_days = e.target.value;
|
||||||
|
setVariations(updated);
|
||||||
|
}}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{__('Override license duration for this variation. 0 = never expires.')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
<Input
|
<Input
|
||||||
placeholder={__('SKU')}
|
placeholder={__('SKU')}
|
||||||
@@ -331,7 +352,7 @@ export function VariationsTab({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Direct Cart Links */}
|
{/* Direct Cart Links */}
|
||||||
{productId && variation.id && (
|
{productId && variation.id && (
|
||||||
<div className="mt-4 pt-4 border-t space-y-2">
|
<div className="mt-4 pt-4 border-t space-y-2">
|
||||||
|
|||||||
@@ -898,6 +898,7 @@ class ProductsController {
|
|||||||
'image_id' => $variation->get_image_id(),
|
'image_id' => $variation->get_image_id(),
|
||||||
'image_url' => $image_url,
|
'image_url' => $image_url,
|
||||||
'image' => $image_url, // For form compatibility
|
'image' => $image_url, // For form compatibility
|
||||||
|
'license_duration_days' => get_post_meta($variation->get_id(), '_license_duration_days', true) ?: '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -994,6 +995,11 @@ class ProductsController {
|
|||||||
$saved_id = $variation->save();
|
$saved_id = $variation->save();
|
||||||
$variations_to_keep[] = $saved_id;
|
$variations_to_keep[] = $saved_id;
|
||||||
|
|
||||||
|
// Save variation-level license duration
|
||||||
|
if (isset($var_data['license_duration_days'])) {
|
||||||
|
update_post_meta($saved_id, '_license_duration_days', self::sanitize_number($var_data['license_duration_days']));
|
||||||
|
}
|
||||||
|
|
||||||
// Manually save attributes using direct database insert
|
// Manually save attributes using direct database insert
|
||||||
if (!empty($wc_attributes)) {
|
if (!empty($wc_attributes)) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|||||||
Reference in New Issue
Block a user