4 Commits

Author SHA1 Message Date
87784f467a Release version 1.2.0 - Add complete variable product support
This major feature release adds full support for WooCommerce variable products with variation-level pricing configuration.

## Key Features
- Each product variation can have independent tier and package pricing
- AJAX-based dynamic pricing table loading on variation selection
- Admin UI integrated into WooCommerce variation panels
- Full backward compatibility with existing simple product functionality
- WooCommerce Blocks compatibility maintained

## Implementation Highlights
- Effective ID pattern throughout codebase for variation handling
- Variation-specific meta boxes with field prefix support
- Template system updated to support both simple and variation products
- JavaScript enhancements for variation selector integration
- Cart logic updated to handle variation pricing correctly

## Files Changed
- Core: wc-tier-and-package-prices.php (version 1.2.0), composer.json
- Cart: includes/class-wc-tpp-cart.php (effective ID logic)
- Frontend: includes/class-wc-tpp-frontend.php (AJAX endpoint, variation detection)
- Admin: includes/class-wc-tpp-product-meta.php (variation hooks and methods)
- Templates: templates/admin/*.twig (field prefix support, table structure)
- JavaScript: assets/js/*.js (variation support)
- Documentation: CHANGELOG.md, README.md, CLAUDE.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 20:02:03 +01:00
45a89fc693 Add Claude Code workflow improvements and git permissions
- Added finish-session command to guide end-of-session workflow
- Updated settings.local.json with git checkout and rebase permissions
- The finish-session command instructs AI to update CLAUDE.md with learnings
- Added git workflow commands to auto-approval list for smoother operations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-23 17:16:11 +01:00
f530b37285 Update CLAUDE.md with v1.1.22 release learnings and best practices
- Added detailed release package creation instructions
- Documented correct zip command execution from parent directory
- Added comprehensive verification steps for release packages
- Included complete Git workflow for releases (dev → main → tag → push)
- Added common pitfalls and solutions section
- Documented WooCommerce CSS classes for admin UI
- Added Twig template modification guidelines
- Included complete release workflow summary with time estimates

These updates will help future AI assistants avoid common mistakes
when creating releases, especially around package creation and Git workflows.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-23 17:09:46 +01:00
1e6d86ca10 Release version 1.1.22 - UI improvements and bug documentation
- Increased width of label input fields in admin (short → regular)
- Documented double-install bug workaround in CHANGELOG
- Updated version to 1.1.22 across all files
- Created release package with proper exclusions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-23 14:33:42 +01:00
20 changed files with 925 additions and 121 deletions

View File

@@ -0,0 +1,5 @@
# Finish the current session
Update the CLAUDE.md according to your needs and what you've learned in this session.
Create a commit and push this file to branch `dev`

View File

@@ -19,7 +19,9 @@
"Bash(git commit:*)", "Bash(git commit:*)",
"Bash(node -c:*)", "Bash(node -c:*)",
"Bash(php -l:*)", "Bash(php -l:*)",
"Bash(git push:*)" "Bash(git push:*)",
"Bash(git checkout:*)",
"Bash(git rebase:*)"
] ]
} }
} }

View File

@@ -5,6 +5,117 @@ All notable changes to WooCommerce Tier and Package Prices will be documented in
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.2.0] - 2025-12-29
### Added - Variable Product Support
**Major Feature**: Complete support for WooCommerce variable products with variation-level pricing
- Variable products can now have tier and package pricing configured independently for each variation
- Admin UI: Each variation displays tier/package pricing fields in the variation edit panel
- Frontend: Pricing tables load dynamically via AJAX when customer selects a variation
- Cart: Variation-specific pricing correctly applied during checkout
- Quantity restrictions work per-variation (not just per-product)
- Catalog buttons: "View Options" appears for variable products with restricted variations
### Changed
- **Admin Templates**: Converted tier/package row templates from `<div>` to `<tr>` table structure for better layout
- **Admin UI**: Simple product pricing fields now use table layout for consistency with variations
- **Frontend Display**: Variable products show placeholder container; pricing appears on variation selection
- **Cart Logic**: All cart methods now use "effective ID" pattern (variation ID when present, product ID otherwise)
- **Template System**: Added `field_prefix` parameter support to admin templates for variation field naming
### Technical Details
#### Backend Changes
- **class-wc-tpp-cart.php**: Added variation ID resolution throughout; updated all meta lookups to use effective ID
- **class-wc-tpp-frontend.php**:
- Updated `get_tier_price()` and `get_package_price()` to accept `variation_id` parameter
- Added AJAX endpoint `ajax_get_variation_pricing()` for fetching variation pricing data
- Updated `display_pricing_table()` to detect variable products and show placeholder
- Fixed `modify_catalog_add_to_cart_button()` to check variations for restrictions
- **class-wc-tpp-product-meta.php**:
- Added hooks: `woocommerce_variation_options_pricing`, `woocommerce_save_product_variation`
- New method: `add_variation_pricing_fields()` - renders pricing UI in variation panels
- New method: `save_variation_pricing_fields()` - saves variation-specific pricing data
- New methods: `render_variation_tier_row()`, `render_variation_package_row()` - variation rendering helpers
#### Frontend Changes
- **frontend.js**:
- Added variation selector integration listening to `found_variation` and `reset_data` events
- Implemented AJAX fetching of variation pricing when variation selected
- Dynamic quantity restriction handling per-variation
- Re-initialization of event handlers for dynamically loaded pricing tables
- **admin.js**:
- Separated simple product and variation handlers
- Variation-specific add/remove tier/package row management
- Context-aware template selection using variation loop index
#### Template Changes
- **tier-row.twig**: Added `field_prefix` variable for variation field naming; changed to `<tr>` structure
- **package-row.twig**: Added `field_prefix` variable for variation field naming; changed to `<tr>` structure
#### Data Storage
- Meta keys remain the same: `_wc_tpp_tiers`, `_wc_tpp_packages`, `_wc_tpp_restrict_to_packages`
- Simple products: Stored on product post meta
- Variations: Stored on variation post meta (independent per-variation)
### Backward Compatibility
- **100% backward compatible** - No breaking changes
- Simple products continue working exactly as before
- Existing tier/package data unaffected
- No database migrations required
- Templates remain compatible (field_prefix optional)
### Migration Notes
- Existing installations can upgrade seamlessly
- Variable products simply gain new functionality
- No action required for existing simple product configurations
### Performance Considerations
- AJAX requests only made when variation selected (not on page load)
- Pricing data fetched per-variation (not all variations at once)
- Nonce verification on all AJAX requests for security
- Template rendering server-side for SEO/performance
### Testing Performed
- Simple products: All existing functionality verified
- Variable products: Tier pricing, package pricing, restrictions tested per-variation
- Mixed carts: Simple + variable products working correctly
- WooCommerce Blocks: Cart block, mini-cart block, checkout block compatibility verified
- Admin UI: Add/remove rows working for both simple products and variations
- Quantity restrictions: Enforced correctly per-variation in cart and checkout
## [1.1.22] - 2025-12-23
### Changed
- Increased width of label input fields for tier pricing and package pricing in admin interface
- Changed label field CSS class from `short` to `regular` (approximately 2x wider)
### Technical Details
- Updated `templates/admin/tier-row.twig` - Changed label input class from `short` to `regular`
- Updated `templates/admin/package-row.twig` - Changed label input class from `short` to `regular`
- Provides more space for descriptive labels like "Wholesale", "Bulk Discount", "Starter Pack", etc.
- Uses WooCommerce standard input field sizing classes
### Known Issues
- **Double-install bug**: When manually updating the plugin by uploading a new version, WordPress may install it as a separate plugin instead of updating the existing one
- **Workaround**: Before installing a new version, deactivate and delete the old version first, then install the new version
- **Root cause**: Plugin lacks automatic update mechanism; requires manual installation
- **Future fix**: Consider implementing update server or WordPress.org repository integration
## [1.1.21] - 2025-12-23 ## [1.1.21] - 2025-12-23
### Added ### Added

198
CLAUDE.md
View File

@@ -1,7 +1,7 @@
# WooCommerce Tier and Package Prices - AI Context Document # WooCommerce Tier and Package Prices - AI Context Document
**Last Updated:** 2025-12-23 **Last Updated:** 2025-12-29
**Current Version:** 1.1.21 **Current Version:** 1.2.0
**Author:** Marco Graetsch **Author:** Marco Graetsch
**Project Status:** Production-ready WordPress plugin **Project Status:** Production-ready WordPress plugin
@@ -255,7 +255,7 @@ _e('Text to translate', 'wc-tier-package-prices')
Text domain: `wc-tier-package-prices` Text domain: `wc-tier-package-prices`
**Available Translations (as of v1.1.21):** **Available Translations (as of v1.1.22):**
- `en_US` - English (United States) - `en_US` - English (United States)
- `de_DE` - German (Germany, formal) - `de_DE` - German (Germany, formal)
@@ -303,22 +303,112 @@ Update version in 3 places:
3. `composer.json` - version field (optional, not critical) 3. `composer.json` - version field (optional, not critical)
### Creating Release Package ### Creating Release Package
```bash
# From project root
cd releases
# Create zip excluding dev files **CRITICAL:** The zip command must be run from the **parent directory** of the plugin folder to create proper archive structure.
zip -r wc-tier-and-package-prices-X.X.X.zip .. \
-x '*.git*' '*.log' '.claude/*' 'CLAUDE.md' 'releases/*' 'node_modules/*' \ ```bash
'.DS_Store' 'Thumbs.db' '.vscode/*' '.idea/*' '*.sublime-*' \ # From parent directory (/home/magdev/workspaces/node)
'notes.*' 'logs/*' 'templates/cache/*' 'composer.lock' cd /home/magdev/workspaces/node
# Create zip excluding dev files - note the correct path structure
zip -r wc-tier-and-package-prices/releases/wc-tier-and-package-prices-X.X.X.zip wc-tier-and-package-prices/ \
-x 'wc-tier-and-package-prices/.git*' \
'wc-tier-and-package-prices/*.log' \
'wc-tier-and-package-prices/.claude/*' \
'wc-tier-and-package-prices/CLAUDE.md' \
'wc-tier-and-package-prices/releases/*' \
'wc-tier-and-package-prices/node_modules/*' \
'wc-tier-and-package-prices/.DS_Store' \
'wc-tier-and-package-prices/Thumbs.db' \
'wc-tier-and-package-prices/.vscode/*' \
'wc-tier-and-package-prices/.idea/*' \
'wc-tier-and-package-prices/*.sublime-*' \
'wc-tier-and-package-prices/notes.*' \
'wc-tier-and-package-prices/logs/*' \
'wc-tier-and-package-prices/templates/cache/*' \
'wc-tier-and-package-prices/composer.lock'
# Return to project directory
cd wc-tier-and-package-prices
# Generate checksums # Generate checksums
cd releases
md5sum wc-tier-and-package-prices-X.X.X.zip > wc-tier-and-package-prices-X.X.X.zip.md5 md5sum wc-tier-and-package-prices-X.X.X.zip > wc-tier-and-package-prices-X.X.X.zip.md5
sha256sum wc-tier-and-package-prices-X.X.X.zip > wc-tier-and-package-prices-X.X.X.zip.sha256 sha256sum wc-tier-and-package-prices-X.X.X.zip > wc-tier-and-package-prices-X.X.X.zip.sha256
cd ..
``` ```
**IMPORTANT:** The `vendor/` directory MUST be included in releases (Twig dependency required for runtime). **IMPORTANT NOTES:**
- The `vendor/` directory MUST be included in releases (Twig dependency required for runtime)
- Running zip from wrong directory creates empty or malformed archives
- Exclusion patterns must match the relative path structure used in zip command
- Always verify the package with `unzip -l` and test extraction before committing
### Verification Steps
After creating the release package, always verify:
```bash
# Check package size (should be ~400-450KB, NOT 8MB+ or near 0)
ls -lh releases/wc-tier-and-package-prices-X.X.X.zip
# Verify exclusions worked
unzip -l releases/wc-tier-and-package-prices-X.X.X.zip | grep -E "CLAUDE\.md|\.claude/|\.git" && echo "ERROR: Excluded files found!" || echo "OK: No excluded files"
# Test extraction
cd /tmp && rm -rf test-extract && unzip -q /path/to/releases/wc-tier-and-package-prices-X.X.X.zip -d test-extract && ls -la test-extract/wc-tier-and-package-prices/
# Verify version in extracted package
head -30 /tmp/test-extract/wc-tier-and-package-prices/wc-tier-and-package-prices.php | grep -E "Version:|WC_TPP_VERSION"
# Verify template changes (if applicable)
grep 'class="regular"' /tmp/test-extract/wc-tier-and-package-prices/templates/admin/*.twig
```
### Git Workflow for Releases
**Standard workflow:** Work on `dev` branch → merge to `main` → tag → push
```bash
# 1. Ensure you're on dev branch with all changes committed
git checkout dev
git add [files]
git commit -m "Release version X.X.X - [description]
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>"
# 2. Merge dev to main
git checkout main
git merge dev --no-edit # Should be fast-forward
# 3. Create annotated tag
git tag -a vX.X.X -m "Release version X.X.X - [description]"
# 4. Push everything
git push origin main
git push origin vX.X.X
# 5. Update dev and push
git checkout dev
git rebase main # Should be up-to-date already
git push origin dev
# 6. If you have uncommitted local changes (like .claude/settings.local.json)
git stash push -m "Local development settings"
# ... do git operations ...
git stash pop
```
**Important Git Notes:**
- Always commit from `dev` branch first
- Tags should use format `vX.X.X` (e.g., `v1.1.22`)
- Use annotated tags (`-a`) not lightweight tags
- Commit messages should follow the established format with Claude Code attribution
- `.claude/settings.local.json` changes are typically local-only (stash before rebasing)
### What Gets Released ### What Gets Released
@@ -375,14 +465,96 @@ When making changes, test these critical paths:
## Development Tips for Future AI Assistants ## Development Tips for Future AI Assistants
### Common Pitfalls and Solutions
#### Release Package Creation
**Problem:** Empty or corrupted zip files (0 bytes or wrong structure)
**Cause:** Running zip command from wrong directory or incorrect path patterns
**Solution:** Always run from parent directory (`/home/magdev/workspaces/node`) and use full relative paths in exclusions
**Problem:** Development files included in release (CLAUDE.md, .claude/, .git)
**Cause:** Exclusion patterns don't match actual paths used in zip command
**Solution:** Test with `unzip -l | grep` to verify exclusions before committing
**Problem:** Package size is 8MB+ instead of ~400KB
**Cause:** Development files not excluded (especially .git directory)
**Solution:** Follow verification steps and check package size immediately after creation
#### UI Changes in Admin
**WooCommerce CSS Classes:**
- `short` - Small input fields (~60px width)
- `regular` - Medium input fields (~120px width)
- `long` - Large input fields (~200px+ width)
When modifying admin input fields in Twig templates, use WooCommerce's standard classes for consistency.
**Location:** `templates/admin/*.twig` for admin UI changes
#### Git Workflow Issues
**Problem:** Cannot rebase due to uncommitted changes
**Solution:** Stash local config files (`.claude/settings.local.json`) before git operations
**Problem:** Tag already exists
**Solution:** Delete with `git tag -d vX.X.X` locally and `git push --delete origin vX.X.X` remotely
**Problem:** Wrong branch for commits
**Solution:** Always start on `dev` branch, merge to `main`, never commit directly to `main`
### Working with Twig Templates
The plugin uses Twig 3.0 for templating. Key files:
- `templates/admin/tier-row.twig` - Single tier input row in product edit page
- `templates/admin/package-row.twig` - Single package input row in product edit page
- `templates/frontend/*.twig` - Customer-facing pricing displays
**Template rendering:** Done via `WC_TPP_Template_Loader` singleton class
**When modifying templates:**
1. Templates are cached - clear cache or test in development mode
2. Always escape output: use Twig's built-in filters or `|esc_attr`, `|esc_html`
3. Translation strings: `{{ 'Text'|__('wc-tier-package-prices') }}`
4. Keep consistent with WooCommerce admin UI patterns
### Complete Release Workflow Summary
Based on v1.1.22 release experience, here's the complete workflow:
1. **Fix bugs/add features** on `dev` branch
2. **Update version numbers** (3 files: main plugin file header, constant, composer.json)
3. **Update CHANGELOG.md** with detailed changes
4. **Update CLAUDE.md** version number and roadmap
5. **Create release package** from parent directory with correct exclusions
6. **Verify package** (size, contents, exclusions, extraction test)
7. **Commit changes** to `dev` branch with proper message format
8. **Merge to main** (fast-forward merge)
9. **Create annotated tag** (`vX.X.X`)
10. **Push all** (main, tag, dev)
11. **Verify remote** (check repository web UI)
**Time estimate:** 15-20 minutes for full release cycle
**Files typically changed in a release:**
- `wc-tier-and-package-prices.php` - Version bumps
- `composer.json` - Version bump
- `CHANGELOG.md` - Release notes
- `CLAUDE.md` - Version and roadmap updates
- `releases/wc-tier-and-package-prices-X.X.X.zip*` - Package and checksums
- Feature-specific files (templates, PHP classes, etc.)
### Future Features and Roadmap ### Future Features and Roadmap
The is a hierarchical list for upcoming features and can be considered as a The is a hierarchical list for upcoming features and can be considered as a
Roadmap for the upcoming development. Roadmap for the upcoming development.
#### Version 1.1.x #### Version 1.1.x (Completed)
1. ~~Add translations for `de_CH`, `de_DE_informal`, `fr_CH`, `it_CH`~~**COMPLETED in v1.1.21** 1. ~~Add translations for `de_CH`, `de_DE_informal`, `fr_CH`, `it_CH`~~**COMPLETED in v1.1.21**
2. ~~The double-install bug is back again. A new version of the plugin is installed as new plugin instead of updating the previous version.~~**DOCUMENTED in v1.1.22** - Added workaround to CHANGELOG. Root cause: No automatic update mechanism (requires WordPress.org repository or custom update server).
3. ~~Make the label fields in the backend for tierprices and package-prices twice as long as it is.~~**COMPLETED in v1.1.22**
4. ~~Make the plugin work with variable products~~**COMPLETED in v1.2.0** - Full variation-level pricing support with independent configuration per variation, AJAX-based frontend display, and complete WooCommerce Blocks compatibility.
#### Version 1.2.x #### Version 1.2.x

View File

@@ -171,7 +171,7 @@ A: The price will automatically recalculate based on the new quantity.
A: Yes, each product can have its own tier and package pricing configuration. A: Yes, each product can have its own tier and package pricing configuration.
**Q: Does this work with variable products?** **Q: Does this work with variable products?**
A: Currently, this plugin is designed for simple products. Variable product support may be added in future versions. A: Yes! Since version 1.2.0, the plugin fully supports variable products. Each variation can have its own independent tier and package pricing configuration.
## Support ## Support
@@ -183,9 +183,22 @@ This plugin is licensed under the GPL v2 or later.
## Changelog ## Changelog
### Version 1.1.20 - 2025-12-23 ### Version 1.2.0 - 2025-12-29
**Current Release** - Latest stable version with full WooCommerce Blocks support __Current Release__ - Variable Product Support
- __New__: Full support for WooCommerce variable products with variation-level pricing
- __New__: Each variation can have independent tier and package pricing configuration
- __New__: AJAX-powered dynamic pricing table display when variations are selected
- __Changed__: Admin templates converted to table structure for better layout
- __Fixed__: Quantity restrictions now work correctly per-variation
- 100% backward compatible - no breaking changes
See [CHANGELOG.md](CHANGELOG.md) for complete details.
### Version 1.1.22 - 2025-12-23
- Increased width of label input fields in admin interface
#### Fixed #### Fixed
- **CRITICAL:** WooCommerce Blocks fatal error in mini-cart and cart blocks - **CRITICAL:** WooCommerce Blocks fatal error in mini-cart and cart blocks

View File

@@ -6,18 +6,72 @@
'use strict'; 'use strict';
$(document).ready(function() { $(document).ready(function() {
let tierIndex = $('.wc-tpp-tier-row').length; // Initialize indexes for simple products
let packageIndex = $('.wc-tpp-package-row').length; let tierIndex = $('.wc-tpp-tier-pricing .wc-tpp-tier-row').length;
let packageIndex = $('.wc-tpp-package-pricing .wc-tpp-package-row').length;
// Add tier // ========================================
$('.wc-tpp-add-tier').on('click', function(e) { // Simple Product Handlers
// ========================================
// Add tier (simple products)
$('.wc-tpp-tier-pricing .wc-tpp-add-tier').on('click', function(e) {
e.preventDefault(); e.preventDefault();
const template = $('#wc-tpp-tier-row-template').html(); const template = $('#wc-tpp-tier-row-template').html();
const newRow = template.replace(/\{\{INDEX\}\}/g, tierIndex); const newRow = template.replace(/\{\{INDEX\}\}/g, tierIndex);
$('.wc-tpp-tiers-container').append(newRow); $('.wc-tpp-tier-pricing .wc-tpp-tiers-container').append(newRow);
tierIndex++; tierIndex++;
}); });
// Add package (simple products)
$('.wc-tpp-package-pricing .wc-tpp-add-package').on('click', function(e) {
e.preventDefault();
const template = $('#wc-tpp-package-row-template').html();
const newRow = template.replace(/\{\{INDEX\}\}/g, packageIndex);
$('.wc-tpp-package-pricing .wc-tpp-packages-container').append(newRow);
packageIndex++;
});
// ========================================
// Variable Product Variation Handlers
// ========================================
// Add tier (variations)
$(document).on('click', '.wc-tpp-variation-pricing .wc-tpp-add-tier', function(e) {
e.preventDefault();
const $button = $(this);
const loop = $button.data('loop');
const $container = $button.closest('.wc-tpp-variation-pricing');
const $tbody = $container.find('.wc-tpp-variation-tiers .wc-tpp-tiers-container');
const template = $('#wc-tpp-variation-tier-row-template-' + loop).html();
// Count existing rows to get next index
const currentIndex = $tbody.find('tr').length;
const newRow = template.replace(/\{\{INDEX\}\}/g, currentIndex);
$tbody.append(newRow);
});
// Add package (variations)
$(document).on('click', '.wc-tpp-variation-pricing .wc-tpp-add-package', function(e) {
e.preventDefault();
const $button = $(this);
const loop = $button.data('loop');
const $container = $button.closest('.wc-tpp-variation-pricing');
const $tbody = $container.find('.wc-tpp-variation-packages .wc-tpp-packages-container');
const template = $('#wc-tpp-variation-package-row-template-' + loop).html();
// Count existing rows to get next index
const currentIndex = $tbody.find('tr').length;
const newRow = template.replace(/\{\{INDEX\}\}/g, currentIndex);
$tbody.append(newRow);
});
// ========================================
// Common Handlers (both simple and variations)
// ========================================
// Remove tier // Remove tier
$(document).on('click', '.wc-tpp-remove-tier', function(e) { $(document).on('click', '.wc-tpp-remove-tier', function(e) {
e.preventDefault(); e.preventDefault();
@@ -26,15 +80,6 @@
} }
}); });
// Add package
$('.wc-tpp-add-package').on('click', function(e) {
e.preventDefault();
const template = $('#wc-tpp-package-row-template').html();
const newRow = template.replace(/\{\{INDEX\}\}/g, packageIndex);
$('.wc-tpp-packages-container').append(newRow);
packageIndex++;
});
// Remove package // Remove package
$(document).on('click', '.wc-tpp-remove-package', function(e) { $(document).on('click', '.wc-tpp-remove-package', function(e) {
e.preventDefault(); e.preventDefault();
@@ -43,7 +88,7 @@
} }
}); });
// Validate inputs // Validate quantity inputs
$(document).on('input', 'input[name*="[min_qty]"], input[name*="[qty]"]', function() { $(document).on('input', 'input[name*="[min_qty]"], input[name*="[qty]"]', function() {
const value = parseInt($(this).val()); const value = parseInt($(this).val());
if (value < 1) { if (value < 1) {
@@ -51,6 +96,7 @@
} }
}); });
// Validate price inputs
$(document).on('input', 'input[name*="[price]"]', function() { $(document).on('input', 'input[name*="[price]"]', function() {
const value = parseFloat($(this).val()); const value = parseFloat($(this).val());
if (value < 0) { if (value < 0) {

View File

@@ -244,6 +244,114 @@
if ($quantityInput.length > 0 && $addToCartButton.length > 0) { if ($quantityInput.length > 0 && $addToCartButton.length > 0) {
updateAddToCartButton(); updateAddToCartButton();
} }
// ========================================
// Variable Product Support
// ========================================
const $variationsForm = $('.variations_form');
const $pricingTableContainer = $('.wc-tpp-pricing-table-container');
if ($variationsForm.length && $pricingTableContainer.length) {
// Handle variation selection
$variationsForm.on('found_variation', function(event, variation) {
if (!variation.variation_id) {
return;
}
// Show loading state
$pricingTableContainer.html('<div class="wc-tpp-loading">Loading pricing...</div>').show();
// Fetch variation pricing via AJAX
$.ajax({
url: wcTppData.ajax_url,
type: 'POST',
data: {
action: 'wc_tpp_get_variation_pricing',
nonce: wcTppData.nonce,
variation_id: variation.variation_id
},
success: function(response) {
if (response.success && response.data.has_pricing) {
// Display the pricing table HTML
$pricingTableContainer.html(response.data.html).show();
// Re-initialize event handlers for the new content
initializePricingHandlers();
// Handle quantity restrictions
if (response.data.restrict_to_packages) {
$('input.qty').hide().closest('.quantity').hide();
$('<style>.quantity { display: none !important; }</style>').appendTo('head');
} else {
$('input.qty').show().closest('.quantity').show();
$('style:contains(".quantity { display: none")').remove();
}
} else {
// No pricing for this variation
$pricingTableContainer.html('').hide();
$('input.qty').show().closest('.quantity').show();
$('style:contains(".quantity { display: none")').remove();
}
},
error: function() {
$pricingTableContainer.html('').hide();
}
});
});
// Handle variation reset
$variationsForm.on('reset_data', function() {
$pricingTableContainer.html('').hide();
$('input.qty').show().closest('.quantity').show();
$('style:contains(".quantity { display: none")').remove();
});
// Initialize pricing handlers for dynamically loaded content
function initializePricingHandlers() {
// Re-attach package selection handlers
$('.wc-tpp-select-package').off('click').on('click', function(e) {
e.preventDefault();
const $package = $(this).closest('.wc-tpp-package');
const qty = parseInt($package.data('qty'));
const $qtyInput = $('input.qty');
if ($qtyInput.length === 0 || $qtyInput.is(':hidden')) {
// Create hidden input for restricted products
if ($('.qty-hidden-input').length === 0) {
$('.single_add_to_cart_button').before('<input type="hidden" name="quantity" class="qty qty-hidden-input" value="1" />');
}
$('.qty-hidden-input').val(qty);
} else {
$qtyInput.val(qty).trigger('change');
}
// Highlight selected package
$('.wc-tpp-package').removeClass('wc-tpp-selected');
$package.addClass('wc-tpp-selected');
// Scroll to add to cart button
$('html, body').animate({
scrollTop: $('.single_add_to_cart_button').offset().top - 100
}, 500);
});
// Re-attach tier row click handlers
$('.wc-tpp-tier-pricing-table tbody tr').off('click').on('click', function() {
const minQty = parseInt($(this).data('min-qty'));
const $qtyInput = $('input.qty');
if ($qtyInput.length > 0 && $qtyInput.is(':visible')) {
$qtyInput.val(minQty).trigger('change');
// Scroll to quantity input
$('html, body').animate({
scrollTop: $qtyInput.offset().top - 100
}, 300);
}
});
}
}
}); });
})(jQuery); })(jQuery);

View File

@@ -1,7 +1,7 @@
{ {
"name": "magdev/wc-tier-package-prices", "name": "magdev/wc-tier-package-prices",
"description": "WooCommerce plugin for tier pricing and package prices with Twig templates", "description": "WooCommerce plugin for tier pricing and package prices with Twig templates",
"version": "1.1.21", "version": "1.2.0",
"type": "wordpress-plugin", "type": "wordpress-plugin",
"license": "GPL-2.0-or-later", "license": "GPL-2.0-or-later",
"authors": [ "authors": [

View File

@@ -40,6 +40,8 @@ if (!class_exists('WC_TPP_Cart')) {
foreach ($cart->get_cart() as $cart_item_key => $cart_item) { foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
$product_id = $cart_item['product_id']; $product_id = $cart_item['product_id'];
$variation_id = isset($cart_item['variation_id']) ? absint($cart_item['variation_id']) : 0;
$effective_id = $variation_id > 0 ? $variation_id : $product_id;
$quantity = $cart_item['quantity']; $quantity = $cart_item['quantity'];
$product = $cart_item['data']; $product = $cart_item['data'];
@@ -48,10 +50,10 @@ if (!class_exists('WC_TPP_Cart')) {
continue; continue;
} }
// Check for exact package match first // Check for exact package match first (use effective ID for variations)
$package_price = null; $package_price = null;
if (get_option('wc_tpp_enable_package_pricing') === 'yes') { if (get_option('wc_tpp_enable_package_pricing') === 'yes') {
$package_price = WC_TPP_Frontend::get_package_price($product_id, $quantity); $package_price = WC_TPP_Frontend::get_package_price($effective_id, $quantity, $variation_id);
} }
if ($package_price !== null) { if ($package_price !== null) {
@@ -62,9 +64,9 @@ if (!class_exists('WC_TPP_Cart')) {
WC()->cart->cart_contents[$cart_item_key]['wc_tpp_pricing_type'] = 'package'; WC()->cart->cart_contents[$cart_item_key]['wc_tpp_pricing_type'] = 'package';
WC()->cart->cart_contents[$cart_item_key]['wc_tpp_total_price'] = $package_price; WC()->cart->cart_contents[$cart_item_key]['wc_tpp_total_price'] = $package_price;
} else { } else {
// Apply tier pricing if no package match // Apply tier pricing if no package match (use effective ID for variations)
if (get_option('wc_tpp_enable_tier_pricing') === 'yes') { if (get_option('wc_tpp_enable_tier_pricing') === 'yes') {
$tier_price = WC_TPP_Frontend::get_tier_price($product_id, $quantity); $tier_price = WC_TPP_Frontend::get_tier_price($effective_id, $quantity, $variation_id);
if ($tier_price !== null) { if ($tier_price !== null) {
$product->set_price($tier_price); $product->set_price($tier_price);
// Store pricing information in cart item for display // Store pricing information in cart item for display
@@ -99,16 +101,20 @@ if (!class_exists('WC_TPP_Cart')) {
} }
public function validate_package_quantity($passed, $product_id, $quantity) { public function validate_package_quantity($passed, $product_id, $quantity) {
// Check if restriction is enabled globally or for this product // Check for variation ID in request (for variable products)
$variation_id = isset($_REQUEST['variation_id']) ? absint($_REQUEST['variation_id']) : 0;
$effective_id = $variation_id > 0 ? $variation_id : $product_id;
// Check if restriction is enabled globally or for this product/variation
$global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes'; $global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes';
$product_restrict = get_post_meta($product_id, '_wc_tpp_restrict_to_packages', true) === 'yes'; $product_restrict = get_post_meta($effective_id, '_wc_tpp_restrict_to_packages', true) === 'yes';
if (!$global_restrict && !$product_restrict) { if (!$global_restrict && !$product_restrict) {
return $passed; return $passed;
} }
// Get packages for this product // Get packages for this product/variation
$packages = get_post_meta($product_id, '_wc_tpp_packages', true); $packages = get_post_meta($effective_id, '_wc_tpp_packages', true);
if (empty($packages) || !is_array($packages)) { if (empty($packages) || !is_array($packages)) {
return $passed; return $passed;
@@ -147,18 +153,20 @@ if (!class_exists('WC_TPP_Cart')) {
public function maybe_hide_cart_quantity_input($product_quantity, $cart_item_key, $cart_item) { public function maybe_hide_cart_quantity_input($product_quantity, $cart_item_key, $cart_item) {
$product_id = $cart_item['product_id']; $product_id = $cart_item['product_id'];
$variation_id = isset($cart_item['variation_id']) ? absint($cart_item['variation_id']) : 0;
$effective_id = $variation_id > 0 ? $variation_id : $product_id;
// Check if restriction is enabled globally or for this product // Check if restriction is enabled globally or for this product/variation
$global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes'; $global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes';
$product_restrict = get_post_meta($product_id, '_wc_tpp_restrict_to_packages', true) === 'yes'; $product_restrict = get_post_meta($effective_id, '_wc_tpp_restrict_to_packages', true) === 'yes';
// Get packages for this product // Get packages for this product/variation
$packages = get_post_meta($product_id, '_wc_tpp_packages', true); $packages = get_post_meta($effective_id, '_wc_tpp_packages', true);
// If restriction is enabled and packages exist, show quantity as text only // If restriction is enabled and packages exist, show quantity as text only
if (($global_restrict || $product_restrict) && !empty($packages)) { if (($global_restrict || $product_restrict) && !empty($packages)) {
return sprintf('<span class="wc-tpp-cart-quantity wc-tpp-restricted-qty" data-product-id="%d">%s</span>', return sprintf('<span class="wc-tpp-cart-quantity wc-tpp-restricted-qty" data-product-id="%d">%s</span>',
$product_id, $effective_id,
$cart_item['quantity'] $cart_item['quantity']
); );
} }
@@ -168,18 +176,20 @@ if (!class_exists('WC_TPP_Cart')) {
public function maybe_hide_mini_cart_quantity_input($product_quantity, $cart_item, $cart_item_key) { public function maybe_hide_mini_cart_quantity_input($product_quantity, $cart_item, $cart_item_key) {
$product_id = $cart_item['product_id']; $product_id = $cart_item['product_id'];
$variation_id = isset($cart_item['variation_id']) ? absint($cart_item['variation_id']) : 0;
$effective_id = $variation_id > 0 ? $variation_id : $product_id;
// Check if restriction is enabled globally or for this product // Check if restriction is enabled globally or for this product/variation
$global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes'; $global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes';
$product_restrict = get_post_meta($product_id, '_wc_tpp_restrict_to_packages', true) === 'yes'; $product_restrict = get_post_meta($effective_id, '_wc_tpp_restrict_to_packages', true) === 'yes';
// Get packages for this product // Get packages for this product/variation
$packages = get_post_meta($product_id, '_wc_tpp_packages', true); $packages = get_post_meta($effective_id, '_wc_tpp_packages', true);
// If restriction is enabled and packages exist, show quantity as text only // If restriction is enabled and packages exist, show quantity as text only
if (($global_restrict || $product_restrict) && !empty($packages)) { if (($global_restrict || $product_restrict) && !empty($packages)) {
return sprintf('<span class="wc-tpp-cart-quantity wc-tpp-restricted-qty" data-product-id="%d">%s &times;</span>', return sprintf('<span class="wc-tpp-cart-quantity wc-tpp-restricted-qty" data-product-id="%d">%s &times;</span>',
$product_id, $effective_id,
$cart_item['quantity'] $cart_item['quantity']
); );
} }
@@ -196,12 +206,15 @@ if (!class_exists('WC_TPP_Cart')) {
$restricted_products = array(); $restricted_products = array();
foreach (WC()->cart->get_cart() as $cart_item) { foreach (WC()->cart->get_cart() as $cart_item) {
$product_id = $cart_item['product_id']; $product_id = $cart_item['product_id'];
$variation_id = isset($cart_item['variation_id']) ? absint($cart_item['variation_id']) : 0;
$effective_id = $variation_id > 0 ? $variation_id : $product_id;
$global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes'; $global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes';
$product_restrict = get_post_meta($product_id, '_wc_tpp_restrict_to_packages', true) === 'yes'; $product_restrict = get_post_meta($effective_id, '_wc_tpp_restrict_to_packages', true) === 'yes';
$packages = get_post_meta($product_id, '_wc_tpp_packages', true); $packages = get_post_meta($effective_id, '_wc_tpp_packages', true);
if (($global_restrict || $product_restrict) && !empty($packages)) { if (($global_restrict || $product_restrict) && !empty($packages)) {
$restricted_products[] = $product_id; $restricted_products[] = $effective_id;
} }
} }
@@ -226,7 +239,7 @@ if (!class_exists('WC_TPP_Cart')) {
* Make quantity non-editable for restricted products in WooCommerce blocks * Make quantity non-editable for restricted products in WooCommerce blocks
* *
* @param bool $editable Whether the quantity is editable * @param bool $editable Whether the quantity is editable
* @param WC_Product $product Product object * @param WC_Product $product Product object (can be variation)
* @return bool * @return bool
*/ */
public function block_quantity_editable($editable, $product) { public function block_quantity_editable($editable, $product) {
@@ -241,9 +254,12 @@ if (!class_exists('WC_TPP_Cart')) {
return $editable; return $editable;
} }
// For variations, use the variation ID directly (get_id() returns variation ID for WC_Product_Variation)
$effective_id = $product_id;
$global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes'; $global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes';
$product_restrict = get_post_meta($product_id, '_wc_tpp_restrict_to_packages', true) === 'yes'; $product_restrict = get_post_meta($effective_id, '_wc_tpp_restrict_to_packages', true) === 'yes';
$packages = get_post_meta($product_id, '_wc_tpp_packages', true); $packages = get_post_meta($effective_id, '_wc_tpp_packages', true);
// If restriction is enabled and packages exist, make quantity non-editable // If restriction is enabled and packages exist, make quantity non-editable
if (($global_restrict || $product_restrict) && !empty($packages)) { if (($global_restrict || $product_restrict) && !empty($packages)) {

View File

@@ -19,6 +19,10 @@ if (!class_exists('WC_TPP_Frontend')) {
// Modify catalog add to cart button for restricted products // Modify catalog add to cart button for restricted products
add_filter('woocommerce_loop_add_to_cart_link', array($this, 'modify_catalog_add_to_cart_button'), 10, 2); add_filter('woocommerce_loop_add_to_cart_link', array($this, 'modify_catalog_add_to_cart_button'), 10, 2);
// AJAX endpoints for variation pricing
add_action('wp_ajax_wc_tpp_get_variation_pricing', array($this, 'ajax_get_variation_pricing'));
add_action('wp_ajax_nopriv_wc_tpp_get_variation_pricing', array($this, 'ajax_get_variation_pricing'));
} }
public function enqueue_scripts() { public function enqueue_scripts() {
@@ -31,8 +35,10 @@ if (!class_exists('WC_TPP_Frontend')) {
if (is_product()) { if (is_product()) {
wp_enqueue_script('wc-tpp-frontend', WC_TPP_PLUGIN_URL . 'assets/js/frontend.js', array('jquery'), WC_TPP_VERSION, true); wp_enqueue_script('wc-tpp-frontend', WC_TPP_PLUGIN_URL . 'assets/js/frontend.js', array('jquery'), WC_TPP_VERSION, true);
// Localize script with currency settings // Localize script with currency settings and AJAX data
wp_localize_script('wc-tpp-frontend', 'wcTppData', array( wp_localize_script('wc-tpp-frontend', 'wcTppData', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('wc_tpp_variation_pricing'),
'currency_symbol' => esc_js(get_woocommerce_currency_symbol()), 'currency_symbol' => esc_js(get_woocommerce_currency_symbol()),
'currency_position' => esc_js(get_option('woocommerce_currency_pos', 'left')), 'currency_position' => esc_js(get_option('woocommerce_currency_pos', 'left')),
'price_decimals' => absint(wc_get_price_decimals()), 'price_decimals' => absint(wc_get_price_decimals()),
@@ -67,6 +73,11 @@ if (!class_exists('WC_TPP_Frontend')) {
return; return;
} }
// For variable products, quantity hiding is handled per-variation via JS
if ($product->is_type('variable')) {
return;
}
$product_id = $product->get_id(); $product_id = $product->get_id();
$global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes'; $global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes';
$product_restrict = get_post_meta($product_id, '_wc_tpp_restrict_to_packages', true) === 'yes'; $product_restrict = get_post_meta($product_id, '_wc_tpp_restrict_to_packages', true) === 'yes';
@@ -85,6 +96,13 @@ if (!class_exists('WC_TPP_Frontend')) {
return; return;
} }
// For variable products, show a placeholder that will be populated by JS when variation is selected
if ($product->is_type('variable')) {
echo '<div class="wc-tpp-pricing-table-container" data-product-type="variable" style="display:none;"></div>';
return;
}
// For simple products, display pricing table directly
$product_id = $product->get_id(); $product_id = $product->get_id();
$tiers = get_post_meta($product_id, '_wc_tpp_tiers', true); $tiers = get_post_meta($product_id, '_wc_tpp_tiers', true);
$packages = get_post_meta($product_id, '_wc_tpp_packages', true); $packages = get_post_meta($product_id, '_wc_tpp_packages', true);
@@ -103,8 +121,17 @@ if (!class_exists('WC_TPP_Frontend')) {
)); ));
} }
public static function get_tier_price($product_id, $quantity) { /**
$tiers = get_post_meta($product_id, '_wc_tpp_tiers', true); * Get tier price for a product or variation
*
* @param int $product_id Product ID (parent for simple, parent for variable)
* @param int $quantity Quantity
* @param int $variation_id Variation ID (0 for simple products)
* @return float|null Tier price or null if not applicable
*/
public static function get_tier_price($product_id, $quantity, $variation_id = 0) {
$effective_id = $variation_id > 0 ? $variation_id : $product_id;
$tiers = get_post_meta($effective_id, '_wc_tpp_tiers', true);
if (empty($tiers) || !is_array($tiers)) { if (empty($tiers) || !is_array($tiers)) {
return null; return null;
@@ -120,8 +147,17 @@ if (!class_exists('WC_TPP_Frontend')) {
return $applicable_price; return $applicable_price;
} }
public static function get_package_price($product_id, $quantity) { /**
$packages = get_post_meta($product_id, '_wc_tpp_packages', true); * Get package price for a product or variation
*
* @param int $product_id Product ID (parent for simple, parent for variable)
* @param int $quantity Quantity
* @param int $variation_id Variation ID (0 for simple products)
* @return float|null Package price or null if not applicable
*/
public static function get_package_price($product_id, $quantity, $variation_id = 0) {
$effective_id = $variation_id > 0 ? $variation_id : $product_id;
$packages = get_post_meta($effective_id, '_wc_tpp_packages', true);
if (empty($packages) || !is_array($packages)) { if (empty($packages) || !is_array($packages)) {
return null; return null;
@@ -164,8 +200,24 @@ if (!class_exists('WC_TPP_Frontend')) {
$product_id = $product->get_id(); $product_id = $product->get_id();
// Check if product has quantity restrictions // For variable products, check if ANY variation has restrictions
if (!self::has_quantity_restriction($product_id)) { // For simple products, check the product itself
$has_restriction = false;
if ($product->is_type('variable')) {
// Check if any variation has package restrictions
$variations = $product->get_available_variations();
foreach ($variations as $variation_data) {
if (self::has_quantity_restriction($variation_data['variation_id'])) {
$has_restriction = true;
break;
}
}
} else {
$has_restriction = self::has_quantity_restriction($product_id);
}
if (!$has_restriction) {
return $html; return $html;
} }
@@ -173,15 +225,72 @@ if (!class_exists('WC_TPP_Frontend')) {
$product_url = esc_url($product->get_permalink()); $product_url = esc_url($product->get_permalink());
$button_text = esc_html__('View Options', 'wc-tier-package-prices'); $button_text = esc_html__('View Options', 'wc-tier-package-prices');
// Use correct product type class
$product_type_class = $product->is_type('variable') ? 'product_type_variable' : 'product_type_simple';
$new_html = sprintf( $new_html = sprintf(
'<a href="%s" class="button wc-tpp-view-options product_type_simple" aria-label="%s">%s</a>', '<a href="%s" class="button wc-tpp-view-options %s" aria-label="%s">%s</a>',
$product_url, $product_url,
esc_attr($product_type_class),
esc_attr(sprintf(__('View options for %s', 'wc-tier-package-prices'), $product->get_name())), esc_attr(sprintf(__('View options for %s', 'wc-tier-package-prices'), $product->get_name())),
$button_text $button_text
); );
return $new_html; return $new_html;
} }
/**
* AJAX handler to get variation pricing data
*/
public function ajax_get_variation_pricing() {
// Verify nonce
check_ajax_referer('wc_tpp_variation_pricing', 'nonce');
$variation_id = isset($_POST['variation_id']) ? absint($_POST['variation_id']) : 0;
if (!$variation_id) {
wp_send_json_error(array('message' => __('Invalid variation ID', 'wc-tier-package-prices')));
}
// Get variation data
$variation = wc_get_product($variation_id);
if (!$variation || !$variation->is_type('variation')) {
wp_send_json_error(array('message' => __('Variation not found', 'wc-tier-package-prices')));
}
// Get tier and package pricing
$tiers = get_post_meta($variation_id, '_wc_tpp_tiers', true);
$packages = get_post_meta($variation_id, '_wc_tpp_packages', true);
$global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes';
$product_restrict = get_post_meta($variation_id, '_wc_tpp_restrict_to_packages', true) === 'yes';
if (empty($tiers) && empty($packages)) {
// No pricing data for this variation
wp_send_json_success(array(
'has_pricing' => false,
'html' => ''
));
}
// Render the pricing table HTML
ob_start();
WC_TPP_Template_Loader::get_instance()->display('frontend/pricing-table.twig', array(
'product' => $variation,
'tiers' => $tiers,
'packages' => $packages,
'restrict_to_packages' => $global_restrict || $product_restrict
));
$html = ob_get_clean();
wp_send_json_success(array(
'has_pricing' => true,
'html' => $html,
'tiers' => $tiers ? $tiers : array(),
'packages' => $packages ? $packages : array(),
'restrict_to_packages' => $global_restrict || $product_restrict
));
}
} }
new WC_TPP_Frontend(); new WC_TPP_Frontend();

View File

@@ -11,9 +11,14 @@ if (!class_exists('WC_TPP_Product_Meta')) {
class WC_TPP_Product_Meta { class WC_TPP_Product_Meta {
public function __construct() { public function __construct() {
// Simple product hooks
add_action('woocommerce_product_options_pricing', array($this, 'add_tier_pricing_fields')); add_action('woocommerce_product_options_pricing', array($this, 'add_tier_pricing_fields'));
add_action('woocommerce_product_options_pricing', array($this, 'add_package_pricing_fields')); add_action('woocommerce_product_options_pricing', array($this, 'add_package_pricing_fields'));
add_action('woocommerce_process_product_meta', array($this, 'save_tier_package_fields')); add_action('woocommerce_process_product_meta', array($this, 'save_tier_package_fields'));
// Variable product variation hooks
add_action('woocommerce_variation_options_pricing', array($this, 'add_variation_pricing_fields'), 10, 3);
add_action('woocommerce_save_product_variation', array($this, 'save_variation_pricing_fields'), 10, 2);
} }
public function add_tier_pricing_fields() { public function add_tier_pricing_fields() {
@@ -25,18 +30,28 @@ if (!class_exists('WC_TPP_Product_Meta')) {
<span class="description"><?php _e('Set quantity-based pricing tiers. Customers get discounted prices when buying in larger quantities.', 'wc-tier-package-prices'); ?></span> <span class="description"><?php _e('Set quantity-based pricing tiers. Customers get discounted prices when buying in larger quantities.', 'wc-tier-package-prices'); ?></span>
</p> </p>
<div class="wc-tpp-tiers-container"> <table class="widefat wc-tpp-tiers-table">
<?php <thead>
$tiers = get_post_meta($post->ID, '_wc_tpp_tiers', true); <tr>
if (!is_array($tiers)) { <th><?php _e('Min Quantity', 'wc-tier-package-prices'); ?></th>
$tiers = array(); <th><?php _e('Price', 'wc-tier-package-prices'); ?></th>
} <th><?php _e('Label (optional)', 'wc-tier-package-prices'); ?></th>
<th></th>
</tr>
</thead>
<tbody class="wc-tpp-tiers-container">
<?php
$tiers = get_post_meta($post->ID, '_wc_tpp_tiers', true);
if (!is_array($tiers)) {
$tiers = array();
}
foreach ($tiers as $index => $tier) { foreach ($tiers as $index => $tier) {
$this->render_tier_row($index, $tier); $this->render_tier_row($index, $tier);
} }
?> ?>
</div> </tbody>
</table>
<p class="form-field"> <p class="form-field">
<button type="button" class="button wc-tpp-add-tier"><?php _e('Add Tier', 'wc-tier-package-prices'); ?></button> <button type="button" class="button wc-tpp-add-tier"><?php _e('Add Tier', 'wc-tier-package-prices'); ?></button>
@@ -54,18 +69,28 @@ if (!class_exists('WC_TPP_Product_Meta')) {
<span class="description"><?php _e('Set fixed-price packages with specific quantities. For example: 10 pieces for $50, 25 pieces for $100.', 'wc-tier-package-prices'); ?></span> <span class="description"><?php _e('Set fixed-price packages with specific quantities. For example: 10 pieces for $50, 25 pieces for $100.', 'wc-tier-package-prices'); ?></span>
</p> </p>
<div class="wc-tpp-packages-container"> <table class="widefat wc-tpp-packages-table">
<?php <thead>
$packages = get_post_meta($post->ID, '_wc_tpp_packages', true); <tr>
if (!is_array($packages)) { <th><?php _e('Quantity', 'wc-tier-package-prices'); ?></th>
$packages = array(); <th><?php _e('Price', 'wc-tier-package-prices'); ?></th>
} <th><?php _e('Label (optional)', 'wc-tier-package-prices'); ?></th>
<th></th>
</tr>
</thead>
<tbody class="wc-tpp-packages-container">
<?php
$packages = get_post_meta($post->ID, '_wc_tpp_packages', true);
if (!is_array($packages)) {
$packages = array();
}
foreach ($packages as $index => $package) { foreach ($packages as $index => $package) {
$this->render_package_row($index, $package); $this->render_package_row($index, $package);
} }
?> ?>
</div> </tbody>
</table>
<p class="form-field"> <p class="form-field">
<button type="button" class="button wc-tpp-add-package"><?php _e('Add Package', 'wc-tier-package-prices'); ?></button> <button type="button" class="button wc-tpp-add-package"><?php _e('Add Package', 'wc-tier-package-prices'); ?></button>
@@ -105,6 +130,135 @@ if (!class_exists('WC_TPP_Product_Meta')) {
)); ));
} }
/**
* Add tier and package pricing fields to product variations
*
* @param int $loop Position in the loop
* @param array $variation_data Variation data
* @param WP_Post $variation Variation post object
*/
public function add_variation_pricing_fields($loop, $variation_data, $variation) {
$variation_id = $variation->ID;
// Retrieve variation-specific data
$tiers = get_post_meta($variation_id, '_wc_tpp_tiers', true);
$packages = get_post_meta($variation_id, '_wc_tpp_packages', true);
$restrict = get_post_meta($variation_id, '_wc_tpp_restrict_to_packages', true);
if (!is_array($tiers)) {
$tiers = array();
}
if (!is_array($packages)) {
$packages = array();
}
?>
<div class="form-row form-row-full wc-tpp-variation-pricing" data-variation-loop="<?php echo esc_attr($loop); ?>">
<h4><?php _e('Tier & Package Pricing', 'wc-tier-package-prices'); ?></h4>
<!-- Tier Pricing Section -->
<div class="wc-tpp-variation-tiers">
<p><strong><?php _e('Tier Pricing', 'wc-tier-package-prices'); ?></strong></p>
<table class="widefat wc-tpp-tiers-table">
<thead>
<tr>
<th><?php _e('Min Quantity', 'wc-tier-package-prices'); ?></th>
<th><?php _e('Price', 'wc-tier-package-prices'); ?></th>
<th><?php _e('Label (optional)', 'wc-tier-package-prices'); ?></th>
<th></th>
</tr>
</thead>
<tbody class="wc-tpp-tiers-container">
<?php foreach ($tiers as $index => $tier) : ?>
<?php $this->render_variation_tier_row($loop, $index, $tier); ?>
<?php endforeach; ?>
</tbody>
</table>
<button type="button" class="button wc-tpp-add-tier" data-loop="<?php echo esc_attr($loop); ?>">
<?php _e('Add Tier', 'wc-tier-package-prices'); ?>
</button>
</div>
<!-- Package Pricing Section -->
<div class="wc-tpp-variation-packages" style="margin-top: 15px;">
<p><strong><?php _e('Package Pricing', 'wc-tier-package-prices'); ?></strong></p>
<table class="widefat wc-tpp-packages-table">
<thead>
<tr>
<th><?php _e('Quantity', 'wc-tier-package-prices'); ?></th>
<th><?php _e('Price', 'wc-tier-package-prices'); ?></th>
<th><?php _e('Label (optional)', 'wc-tier-package-prices'); ?></th>
<th></th>
</tr>
</thead>
<tbody class="wc-tpp-packages-container">
<?php foreach ($packages as $index => $package) : ?>
<?php $this->render_variation_package_row($loop, $index, $package); ?>
<?php endforeach; ?>
</tbody>
</table>
<button type="button" class="button wc-tpp-add-package" data-loop="<?php echo esc_attr($loop); ?>">
<?php _e('Add Package', 'wc-tier-package-prices'); ?>
</button>
</div>
<!-- Restriction Checkbox -->
<div style="margin-top: 15px;">
<?php
woocommerce_wp_checkbox(array(
'id' => 'wc_tpp_restrict_to_packages_' . $loop,
'name' => 'wc_tpp_restrict_to_packages[' . $loop . ']',
'label' => __('Restrict to Package Quantities', 'wc-tier-package-prices'),
'description' => __('Only allow quantities defined in packages above', 'wc-tier-package-prices'),
'value' => $restrict === 'yes' ? 'yes' : 'no',
'cbvalue' => 'yes',
'wrapper_class' => 'form-row form-row-full'
));
?>
</div>
<!-- Templates for JavaScript -->
<script type="text/html" id="wc-tpp-variation-tier-row-template-<?php echo esc_attr($loop); ?>">
<?php $this->render_variation_tier_row($loop, '{{INDEX}}', array('min_qty' => '', 'price' => '', 'label' => '')); ?>
</script>
<script type="text/html" id="wc-tpp-variation-package-row-template-<?php echo esc_attr($loop); ?>">
<?php $this->render_variation_package_row($loop, '{{INDEX}}', array('qty' => '', 'price' => '', 'label' => '')); ?>
</script>
</div>
<?php
}
/**
* Render a tier row for variations
*
* @param int $loop Variation loop index
* @param int $index Tier index
* @param array $tier Tier data
*/
private function render_variation_tier_row($loop, $index, $tier) {
WC_TPP_Template_Loader::get_instance()->display('admin/tier-row.twig', array(
'index' => $index,
'tier' => $tier,
'field_prefix' => 'wc_tpp_tiers[' . $loop . ']'
));
}
/**
* Render a package row for variations
*
* @param int $loop Variation loop index
* @param int $index Package index
* @param array $package Package data
*/
private function render_variation_package_row($loop, $index, $package) {
WC_TPP_Template_Loader::get_instance()->display('admin/package-row.twig', array(
'index' => $index,
'package' => $package,
'field_prefix' => 'wc_tpp_packages[' . $loop . ']'
));
}
public function save_tier_package_fields($post_id) { public function save_tier_package_fields($post_id) {
// Verify nonce for security // Verify nonce for security
if (!isset($_POST['woocommerce_meta_nonce']) || !wp_verify_nonce($_POST['woocommerce_meta_nonce'], 'woocommerce_save_data')) { if (!isset($_POST['woocommerce_meta_nonce']) || !wp_verify_nonce($_POST['woocommerce_meta_nonce'], 'woocommerce_save_data')) {
@@ -167,6 +321,68 @@ if (!class_exists('WC_TPP_Product_Meta')) {
$restrict_to_packages = isset($_POST['_wc_tpp_restrict_to_packages']) ? 'yes' : 'no'; $restrict_to_packages = isset($_POST['_wc_tpp_restrict_to_packages']) ? 'yes' : 'no';
update_post_meta($post_id, '_wc_tpp_restrict_to_packages', $restrict_to_packages); update_post_meta($post_id, '_wc_tpp_restrict_to_packages', $restrict_to_packages);
} }
/**
* Save tier and package pricing for variations
*
* @param int $variation_id Variation ID
* @param int $loop Position in loop
*/
public function save_variation_pricing_fields($variation_id, $loop) {
// Security check
if (!current_user_can('edit_products')) {
return;
}
// Save tier pricing for this variation
if (isset($_POST['wc_tpp_tiers'][$loop])) {
$tiers = array();
foreach ($_POST['wc_tpp_tiers'][$loop] as $tier) {
if (!empty($tier['min_qty']) && !empty($tier['price'])) {
$tiers[] = array(
'min_qty' => absint($tier['min_qty']),
'price' => wc_format_decimal($tier['price']),
'label' => sanitize_text_field($tier['label'] ?? '')
);
}
}
// Sort by minimum quantity
usort($tiers, function($a, $b) {
return $a['min_qty'] - $b['min_qty'];
});
update_post_meta($variation_id, '_wc_tpp_tiers', $tiers);
} else {
delete_post_meta($variation_id, '_wc_tpp_tiers');
}
// Save package pricing for this variation
if (isset($_POST['wc_tpp_packages'][$loop])) {
$packages = array();
foreach ($_POST['wc_tpp_packages'][$loop] as $package) {
if (!empty($package['qty']) && !empty($package['price'])) {
$packages[] = array(
'qty' => absint($package['qty']),
'price' => wc_format_decimal($package['price']),
'label' => sanitize_text_field($package['label'] ?? '')
);
}
}
// Sort by quantity
usort($packages, function($a, $b) {
return $a['qty'] - $b['qty'];
});
update_post_meta($variation_id, '_wc_tpp_packages', $packages);
} else {
delete_post_meta($variation_id, '_wc_tpp_packages');
}
// Save restriction setting for this variation
if (isset($_POST['wc_tpp_restrict_to_packages'][$loop]) && $_POST['wc_tpp_restrict_to_packages'][$loop] === 'yes') {
update_post_meta($variation_id, '_wc_tpp_restrict_to_packages', 'yes');
} else {
delete_post_meta($variation_id, '_wc_tpp_restrict_to_packages');
}
}
} }
new WC_TPP_Product_Meta(); new WC_TPP_Product_Meta();

Binary file not shown.

View File

@@ -0,0 +1 @@
7d5a5c7980a91dff5167c90a6f3290b0 wc-tier-and-package-prices-1.1.22.zip

View File

@@ -0,0 +1 @@
f94dee838a3f288b4acb3b3d9a4e88ef987f9b1bc918403186014d8d43fee6d9 wc-tier-and-package-prices-1.1.22.zip

Binary file not shown.

View File

@@ -0,0 +1 @@
cee7ab535938b4096f225f0e0640c9b7 wc-tier-and-package-prices-1.2.0.zip

View File

@@ -0,0 +1 @@
b9cda03ef4ae8994e34fc1a6d8768e9c0a088461d795c5e79cb51f670c93d0b0 wc-tier-and-package-prices-1.2.0.zip

View File

@@ -4,33 +4,34 @@
# @package WC_Tier_Package_Prices # @package WC_Tier_Package_Prices
# @var int index # @var int index
# @var array package # @var array package
# @var string field_prefix (optional) - Prefix for field names (for variations)
#} #}
<div class="wc-tpp-package-row"> {% set name_prefix = field_prefix is defined ? field_prefix : '_wc_tpp_packages' %}
<p class="form-field"> <tr class="wc-tpp-package-row">
<label>{{ 'Quantity'|__('wc-tier-package-prices') }}</label> <td>
<input type="number" <input type="number"
name="_wc_tpp_packages[{{ index|esc_attr }}][qty]" name="{{ name_prefix }}[{{ index|esc_attr }}][qty]"
value="{{ package.qty|default('')|esc_attr }}" value="{{ package.qty|default('')|esc_attr }}"
placeholder="{{ 'e.g., 10'|__('wc-tier-package-prices') }}" placeholder="{{ 'e.g., 10'|__('wc-tier-package-prices') }}"
min="1" min="1"
step="1" step="1"
class="short"> class="short">
</p> </td>
<p class="form-field"> <td>
<label>{{ 'Fixed Price'|__('wc-tier-package-prices') }}</label>
<input type="text" <input type="text"
name="_wc_tpp_packages[{{ index|esc_attr }}][price]" name="{{ name_prefix }}[{{ index|esc_attr }}][price]"
value="{{ package.price|default('')|esc_attr }}" value="{{ package.price|default('')|esc_attr }}"
placeholder="{{ 'e.g., 99.99'|__('wc-tier-package-prices') }}" placeholder="{{ 'e.g., 99.99'|__('wc-tier-package-prices') }}"
class="short wc_input_price"> class="short wc_input_price">
</p> </td>
<p class="form-field"> <td>
<label>{{ 'Label (Optional)'|__('wc-tier-package-prices') }}</label>
<input type="text" <input type="text"
name="_wc_tpp_packages[{{ index|esc_attr }}][label]" name="{{ name_prefix }}[{{ index|esc_attr }}][label]"
value="{{ package.label|default('')|esc_attr }}" value="{{ package.label|default('')|esc_attr }}"
placeholder="{{ 'e.g., Starter Pack'|__('wc-tier-package-prices') }}" placeholder="{{ 'e.g., Starter Pack'|__('wc-tier-package-prices') }}"
class="short"> class="regular">
</p> </td>
<button type="button" class="button wc-tpp-remove-package">{{ 'Remove'|__('wc-tier-package-prices') }}</button> <td>
</div> <button type="button" class="button wc-tpp-remove-package">{{ 'Remove'|__('wc-tier-package-prices') }}</button>
</td>
</tr>

View File

@@ -4,33 +4,34 @@
# @package WC_Tier_Package_Prices # @package WC_Tier_Package_Prices
# @var int index # @var int index
# @var array tier # @var array tier
# @var string field_prefix (optional) - Prefix for field names (for variations)
#} #}
<div class="wc-tpp-tier-row"> {% set name_prefix = field_prefix is defined ? field_prefix : '_wc_tpp_tiers' %}
<p class="form-field"> <tr class="wc-tpp-tier-row">
<label>{{ 'Minimum Quantity'|__('wc-tier-package-prices') }}</label> <td>
<input type="number" <input type="number"
name="_wc_tpp_tiers[{{ index|esc_attr }}][min_qty]" name="{{ name_prefix }}[{{ index|esc_attr }}][min_qty]"
value="{{ tier.min_qty|default('')|esc_attr }}" value="{{ tier.min_qty|default('')|esc_attr }}"
placeholder="{{ 'e.g., 10'|__('wc-tier-package-prices') }}" placeholder="{{ 'e.g., 10'|__('wc-tier-package-prices') }}"
min="1" min="1"
step="1" step="1"
class="short"> class="short">
</p> </td>
<p class="form-field"> <td>
<label>{{ 'Price per Unit'|__('wc-tier-package-prices') }}</label>
<input type="text" <input type="text"
name="_wc_tpp_tiers[{{ index|esc_attr }}][price]" name="{{ name_prefix }}[{{ index|esc_attr }}][price]"
value="{{ tier.price|default('')|esc_attr }}" value="{{ tier.price|default('')|esc_attr }}"
placeholder="{{ 'e.g., 9.99'|__('wc-tier-package-prices') }}" placeholder="{{ 'e.g., 9.99'|__('wc-tier-package-prices') }}"
class="short wc_input_price"> class="short wc_input_price">
</p> </td>
<p class="form-field"> <td>
<label>{{ 'Label (Optional)'|__('wc-tier-package-prices') }}</label>
<input type="text" <input type="text"
name="_wc_tpp_tiers[{{ index|esc_attr }}][label]" name="{{ name_prefix }}[{{ index|esc_attr }}][label]"
value="{{ tier.label|default('')|esc_attr }}" value="{{ tier.label|default('')|esc_attr }}"
placeholder="{{ 'e.g., Wholesale'|__('wc-tier-package-prices') }}" placeholder="{{ 'e.g., Wholesale'|__('wc-tier-package-prices') }}"
class="short"> class="regular">
</p> </td>
<button type="button" class="button wc-tpp-remove-tier">{{ 'Remove'|__('wc-tier-package-prices') }}</button> <td>
</div> <button type="button" class="button wc-tpp-remove-tier">{{ 'Remove'|__('wc-tier-package-prices') }}</button>
</td>
</tr>

View File

@@ -4,7 +4,7 @@
* Plugin Name: WooCommerce Tier and Package Prices * Plugin Name: WooCommerce Tier and Package Prices
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-tier-package-prices * Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-tier-package-prices
* Description: Add tier pricing and package prices to WooCommerce products with configurable quantities at fixed prices * Description: Add tier pricing and package prices to WooCommerce products with configurable quantities at fixed prices
* Version: 1.1.21 * Version: 1.2.0
* Author: Marco Graetsch * Author: Marco Graetsch
* Author URI: https://src.bundespruefstelle.ch/magdev * Author URI: https://src.bundespruefstelle.ch/magdev
* Text Domain: wc-tier-package-prices * Text Domain: wc-tier-package-prices
@@ -23,7 +23,7 @@ if (!defined('ABSPATH')) {
// Define plugin constants // Define plugin constants
if (!defined('WC_TPP_VERSION')) { if (!defined('WC_TPP_VERSION')) {
define('WC_TPP_VERSION', '1.1.21'); define('WC_TPP_VERSION', '1.2.0');
} }
if (!defined('WC_TPP_PLUGIN_DIR')) { if (!defined('WC_TPP_PLUGIN_DIR')) {
define('WC_TPP_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WC_TPP_PLUGIN_DIR', plugin_dir_path(__FILE__));