feat: Implement A4 invoice layout and hide Label for virtual orders
Invoice: - Enhanced A4-ready layout with proper structure - Store header with invoice number - Billing/shipping address sections - Styled items table with alternating rows - Totals summary with conditional display - Thank you footer Label: - Label button now hidden for virtual-only orders - Uses existing isVirtualOnly detection Print CSS: - Added @page A4 size directive - Print-color-adjust for background colors - 20mm padding for proper margins Documentation: - Updated subscription module plan (comprehensive) - Updated affiliate module plan (comprehensive) - Created shipping label standardization plan
This commit is contained in:
241
.agent/plans/affiliate-module.md
Normal file
241
.agent/plans/affiliate-module.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# Affiliate Module Plan
|
||||
|
||||
## Overview
|
||||
Referral tracking with hybrid customer/affiliate roles, integrated as a core plugin module.
|
||||
|
||||
---
|
||||
|
||||
## Module Architecture
|
||||
|
||||
### Core Features
|
||||
- **Affiliate registration**: Customers can become affiliates
|
||||
- **Approval workflow**: Manual or auto-approval of affiliates
|
||||
- **Unique referral links/codes**: Each affiliate gets unique tracking
|
||||
- **Commission tracking**: Track referrals and calculate earnings
|
||||
- **Tiered commission rates**: Different rates per product/category/affiliate level
|
||||
- **Payout management**: Track and process affiliate payouts
|
||||
- **Affiliate dashboard**: Self-service stats and link generator
|
||||
|
||||
### Hybrid Roles
|
||||
- A customer can also be an affiliate
|
||||
- No separate user type; affiliate data linked to existing user
|
||||
- Affiliates can still make purchases (self-referral rules configurable)
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Table: `woonoow_affiliates`
|
||||
```sql
|
||||
CREATE TABLE woonoow_affiliates (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT UNSIGNED NOT NULL UNIQUE,
|
||||
status ENUM('pending', 'active', 'rejected', 'suspended') DEFAULT 'pending',
|
||||
referral_code VARCHAR(50) NOT NULL UNIQUE,
|
||||
commission_rate DECIMAL(5,2) DEFAULT NULL, -- Override global rate
|
||||
tier_id BIGINT UNSIGNED DEFAULT NULL,
|
||||
payment_email VARCHAR(255) DEFAULT NULL,
|
||||
payment_method VARCHAR(50) DEFAULT NULL,
|
||||
total_earnings DECIMAL(15,2) DEFAULT 0,
|
||||
total_unpaid DECIMAL(15,2) DEFAULT 0,
|
||||
total_paid DECIMAL(15,2) DEFAULT 0,
|
||||
referral_count INT UNSIGNED DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_referral_code (referral_code)
|
||||
);
|
||||
```
|
||||
|
||||
### Table: `woonoow_referrals`
|
||||
```sql
|
||||
CREATE TABLE woonoow_referrals (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
affiliate_id BIGINT UNSIGNED NOT NULL,
|
||||
order_id BIGINT UNSIGNED NOT NULL,
|
||||
customer_id BIGINT UNSIGNED DEFAULT NULL,
|
||||
subtotal DECIMAL(15,2) NOT NULL,
|
||||
commission_rate DECIMAL(5,2) NOT NULL,
|
||||
commission_amount DECIMAL(15,2) NOT NULL,
|
||||
status ENUM('pending', 'approved', 'rejected', 'paid') DEFAULT 'pending',
|
||||
referral_type ENUM('link', 'code', 'coupon') DEFAULT 'link',
|
||||
ip_address VARCHAR(45) DEFAULT NULL,
|
||||
user_agent TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
approved_at DATETIME DEFAULT NULL,
|
||||
paid_at DATETIME DEFAULT NULL,
|
||||
INDEX idx_affiliate (affiliate_id),
|
||||
INDEX idx_order (order_id),
|
||||
INDEX idx_status (status)
|
||||
);
|
||||
```
|
||||
|
||||
### Table: `woonoow_payouts`
|
||||
```sql
|
||||
CREATE TABLE woonoow_payouts (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
affiliate_id BIGINT UNSIGNED NOT NULL,
|
||||
amount DECIMAL(15,2) NOT NULL,
|
||||
method VARCHAR(50) DEFAULT NULL,
|
||||
reference VARCHAR(255) DEFAULT NULL, -- Bank ref, PayPal transaction, etc.
|
||||
status ENUM('pending', 'processing', 'completed', 'failed') DEFAULT 'pending',
|
||||
notes TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at DATETIME DEFAULT NULL,
|
||||
INDEX idx_affiliate (affiliate_id),
|
||||
INDEX idx_status (status)
|
||||
);
|
||||
```
|
||||
|
||||
### Table: `woonoow_affiliate_tiers` (Optional)
|
||||
```sql
|
||||
CREATE TABLE woonoow_affiliate_tiers (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
commission_rate DECIMAL(5,2) NOT NULL,
|
||||
min_referrals INT UNSIGNED DEFAULT 0, -- Auto-promote at X referrals
|
||||
min_earnings DECIMAL(15,2) DEFAULT 0, -- Auto-promote at X earnings
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backend Structure
|
||||
|
||||
```
|
||||
includes/Modules/Affiliate/
|
||||
├── AffiliateModule.php # Bootstrap, hooks, tracking
|
||||
├── AffiliateManager.php # Core logic
|
||||
├── ReferralTracker.php # Track referral cookies/links
|
||||
└── AffiliateSettings.php # Settings schema
|
||||
|
||||
includes/Api/AffiliatesController.php # REST endpoints
|
||||
```
|
||||
|
||||
### AffiliateManager Methods
|
||||
```php
|
||||
- register($user_id) # Register user as affiliate
|
||||
- approve($affiliate_id) # Approve pending affiliate
|
||||
- reject($affiliate_id, $reason) # Reject application
|
||||
- suspend($affiliate_id) # Suspend affiliate
|
||||
- track_referral($order, $affiliate) # Create referral record
|
||||
- calculate_commission($order, $affiliate) # Calculate earnings
|
||||
- approve_referral($referral_id) # Approve pending referral
|
||||
- create_payout($affiliate_id, $amount) # Create payout request
|
||||
- process_payout($payout_id) # Mark payout complete
|
||||
- get_affiliate_stats($affiliate_id) # Dashboard stats
|
||||
```
|
||||
|
||||
### Referral Tracking
|
||||
1. Affiliate shares link: `yourstore.com/?ref=CODE`
|
||||
2. ReferralTracker sets cookie: `wnw_ref=CODE` (30 days default)
|
||||
3. On checkout, check cookie and link to affiliate
|
||||
4. On order completion, create referral record
|
||||
|
||||
---
|
||||
|
||||
## REST API Endpoints
|
||||
|
||||
### Admin Endpoints
|
||||
```
|
||||
GET /affiliates # List affiliates
|
||||
GET /affiliates/{id} # Affiliate details
|
||||
PUT /affiliates/{id} # Update affiliate
|
||||
POST /affiliates/{id}/approve # Approve affiliate
|
||||
POST /affiliates/{id}/reject # Reject affiliate
|
||||
GET /affiliates/referrals # All referrals
|
||||
GET /affiliates/payouts # All payouts
|
||||
POST /affiliates/payouts/{id}/complete # Complete payout
|
||||
```
|
||||
|
||||
### Customer/Affiliate Endpoints
|
||||
```
|
||||
GET /my-affiliate # Check if affiliate
|
||||
POST /my-affiliate/register # Register as affiliate
|
||||
GET /my-affiliate/stats # Dashboard stats
|
||||
GET /my-affiliate/referrals # My referrals
|
||||
GET /my-affiliate/payouts # My payouts
|
||||
POST /my-affiliate/payout-request # Request payout
|
||||
GET /my-affiliate/links # Generate referral links
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Admin SPA
|
||||
|
||||
### Affiliates List (`/marketing/affiliates`)
|
||||
- Table: Name, Email, Status, Referrals, Earnings, Actions
|
||||
- Filters: Status, Date range
|
||||
- Actions: Approve, Reject, View
|
||||
|
||||
### Affiliate Detail (`/marketing/affiliates/:id`)
|
||||
- Affiliate info card
|
||||
- Stats summary
|
||||
- Referrals list
|
||||
- Payouts history
|
||||
- Action buttons
|
||||
|
||||
### Referrals List (`/marketing/affiliates/referrals`)
|
||||
- All referrals across affiliates
|
||||
- Filters: Status, Affiliate, Date
|
||||
|
||||
### Payouts (`/marketing/affiliates/payouts`)
|
||||
- Payout requests
|
||||
- Process payouts
|
||||
- Payment history
|
||||
|
||||
---
|
||||
|
||||
## Customer SPA
|
||||
|
||||
### Become an Affiliate (`/my-account/affiliate`)
|
||||
- Registration form (if not affiliate)
|
||||
- Dashboard (if affiliate)
|
||||
|
||||
### Affiliate Dashboard
|
||||
- Stats: Total Referrals, Pending, Approved, Earnings
|
||||
- Referral link generator
|
||||
- Recent referrals
|
||||
- Payout request button
|
||||
|
||||
### My Referrals
|
||||
- List of referrals with status
|
||||
- Commission amount
|
||||
|
||||
### My Payouts
|
||||
- Payout history
|
||||
- Pending amount
|
||||
- Request payout form
|
||||
|
||||
---
|
||||
|
||||
## Settings Schema
|
||||
|
||||
```php
|
||||
return [
|
||||
'enabled' => true,
|
||||
'registration_type' => 'open', // open, approval, invite
|
||||
'auto_approve' => false,
|
||||
'default_commission_rate' => 10, // 10%
|
||||
'commission_type' => 'percentage', // percentage, flat
|
||||
'cookie_duration' => 30, // days
|
||||
'min_payout_amount' => 50,
|
||||
'payout_methods' => ['bank_transfer', 'paypal'],
|
||||
'allow_self_referral' => false,
|
||||
'referral_approval' => 'auto', // auto, manual
|
||||
'approval_delay_days' => 14, // Wait X days before auto-approve
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Priority
|
||||
1. Database tables and AffiliateManager
|
||||
2. ReferralTracker (cookie-based tracking)
|
||||
3. Order hook to create referrals
|
||||
4. Admin SPA affiliates management
|
||||
5. Customer SPA affiliate dashboard
|
||||
6. Payout management
|
||||
7. Tier system (optional)
|
||||
84
.agent/plans/shipping-label.md
Normal file
84
.agent/plans/shipping-label.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Shipping Label Plan
|
||||
|
||||
## Overview
|
||||
Standardized waybill data structure for shipping label generation.
|
||||
|
||||
## Problem
|
||||
- Different shipping carrier addons (JNE, JNT, SiCepat, etc.) store data differently
|
||||
- No standard structure for label generation
|
||||
- Label button needs waybill data to function
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
### 1. Standardized Meta Key
|
||||
Order meta: `_shipping_waybill`
|
||||
|
||||
### 2. Data Structure
|
||||
```json
|
||||
{
|
||||
"tracking_number": "JNE123456789",
|
||||
"carrier": "jne",
|
||||
"carrier_name": "JNE Express",
|
||||
"service": "REG",
|
||||
"estimated_days": 3,
|
||||
"sender": {
|
||||
"name": "Store Name",
|
||||
"address": "Full address line 1",
|
||||
"city": "Jakarta",
|
||||
"postcode": "12345",
|
||||
"phone": "08123456789"
|
||||
},
|
||||
"recipient": {
|
||||
"name": "Customer Name",
|
||||
"address": "Full address line 1",
|
||||
"city": "Bandung",
|
||||
"postcode": "40123",
|
||||
"phone": "08987654321"
|
||||
},
|
||||
"package": {
|
||||
"weight": "1.5",
|
||||
"weight_unit": "kg",
|
||||
"dimensions": "20x15x10",
|
||||
"dimensions_unit": "cm"
|
||||
},
|
||||
"label_url": null,
|
||||
"barcode": "JNE123456789",
|
||||
"barcode_type": "128",
|
||||
"created_at": "2026-01-05T12:00:00+07:00"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Addon Integration Contract
|
||||
|
||||
Shipping addons MUST:
|
||||
1. Call `update_post_meta($order_id, '_shipping_waybill', $waybill_data)`
|
||||
2. Use the standard structure above
|
||||
3. Set `label_url` if carrier provides downloadable PDF
|
||||
4. Set `barcode` for local label generation
|
||||
|
||||
### 4. Label Button Behavior
|
||||
1. Check if `_shipping_waybill` meta exists on order
|
||||
2. If `label_url` → open carrier's PDF
|
||||
3. Otherwise → generate printable label from meta data
|
||||
|
||||
### 5. UI Behavior
|
||||
- Label button hidden if order is virtual-only
|
||||
- Label button shows "Generate Label" if no waybill yet
|
||||
- Label button shows "Print Label" if waybill exists
|
||||
|
||||
## API Endpoint (Future)
|
||||
```
|
||||
POST /woonoow/v1/orders/{id}/generate-waybill
|
||||
- Calls shipping carrier API
|
||||
- Stores waybill in standardized format
|
||||
- Returns waybill data
|
||||
|
||||
GET /woonoow/v1/orders/{id}/waybill
|
||||
- Returns current waybill data
|
||||
```
|
||||
|
||||
## Implementation Priority
|
||||
1. Define standard structure (this document)
|
||||
2. Implement Label UI conditional logic
|
||||
3. Create waybill API endpoint
|
||||
4. Document for addon developers
|
||||
191
.agent/plans/subscription-module.md
Normal file
191
.agent/plans/subscription-module.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# Subscription Module Plan
|
||||
|
||||
## Overview
|
||||
Recurring product subscriptions with flexible billing, integrated as a core plugin module (like Newsletter/Wishlist/Licensing).
|
||||
|
||||
---
|
||||
|
||||
## Module Architecture
|
||||
|
||||
### Core Features
|
||||
- **Recurring billing**: Weekly, monthly, yearly, custom intervals
|
||||
- **Free trials**: X days free before billing starts
|
||||
- **Sign-up fees**: One-time fee on first subscription
|
||||
- **Automatic renewals**: Process payment on renewal date
|
||||
- **Manual renewal**: Allow customers to renew manually
|
||||
- **Proration**: Calculate prorated amounts on plan changes
|
||||
- **Pause/Resume**: Allow customers to pause subscriptions
|
||||
|
||||
### Product Integration
|
||||
- Checkbox under "Additional Options": **Enable subscription for this product**
|
||||
- When enabled, show subscription settings:
|
||||
- Billing period (weekly/monthly/yearly/custom)
|
||||
- Billing interval (every X periods)
|
||||
- Free trial days
|
||||
- Sign-up fee
|
||||
- Subscription length (0 = unlimited)
|
||||
- Variable products: Variation-level subscription settings (different durations/prices per variation)
|
||||
|
||||
### Integration with Licensing
|
||||
- Licenses can be bound to subscriptions
|
||||
- When subscription is active → license is valid
|
||||
- When subscription expires/cancelled → license is revoked
|
||||
- Auto-renewal keeps license active
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Table: `woonoow_subscriptions`
|
||||
```sql
|
||||
CREATE TABLE woonoow_subscriptions (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
order_id BIGINT UNSIGNED NOT NULL,
|
||||
product_id BIGINT UNSIGNED NOT NULL,
|
||||
variation_id BIGINT UNSIGNED DEFAULT NULL,
|
||||
status ENUM('pending', 'active', 'on-hold', 'cancelled', 'expired', 'pending-cancel') DEFAULT 'pending',
|
||||
billing_period ENUM('day', 'week', 'month', 'year') NOT NULL,
|
||||
billing_interval INT UNSIGNED DEFAULT 1,
|
||||
start_date DATETIME NOT NULL,
|
||||
trial_end_date DATETIME DEFAULT NULL,
|
||||
next_payment_date DATETIME DEFAULT NULL,
|
||||
end_date DATETIME DEFAULT NULL,
|
||||
last_payment_date DATETIME DEFAULT NULL,
|
||||
payment_method VARCHAR(100) DEFAULT NULL,
|
||||
payment_meta LONGTEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_order_id (order_id),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_next_payment (next_payment_date)
|
||||
);
|
||||
```
|
||||
|
||||
### Table: `woonoow_subscription_orders`
|
||||
Links subscription to renewal orders:
|
||||
```sql
|
||||
CREATE TABLE woonoow_subscription_orders (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
subscription_id BIGINT UNSIGNED NOT NULL,
|
||||
order_id BIGINT UNSIGNED NOT NULL,
|
||||
order_type ENUM('parent', 'renewal', 'switch', 'resubscribe') DEFAULT 'renewal',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_subscription (subscription_id),
|
||||
INDEX idx_order (order_id)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backend Structure
|
||||
|
||||
```
|
||||
includes/Modules/Subscription/
|
||||
├── SubscriptionModule.php # Bootstrap, hooks, product meta
|
||||
├── SubscriptionManager.php # Core logic
|
||||
├── SubscriptionScheduler.php # Cron jobs for renewals
|
||||
└── SubscriptionSettings.php # Settings schema
|
||||
|
||||
includes/Api/SubscriptionsController.php # REST endpoints
|
||||
```
|
||||
|
||||
### SubscriptionManager Methods
|
||||
```php
|
||||
- create($order, $product, $user) # Create subscription from order
|
||||
- renew($subscription_id) # Process renewal
|
||||
- cancel($subscription_id, $reason) # Cancel subscription
|
||||
- pause($subscription_id) # Pause subscription
|
||||
- resume($subscription_id) # Resume paused subscription
|
||||
- switch($subscription_id, $new_product) # Switch plan
|
||||
- get_next_payment_date($subscription) # Calculate next date
|
||||
- process_renewal_payment($subscription) # Charge payment
|
||||
```
|
||||
|
||||
### Cron Jobs
|
||||
- `woonoow_process_subscription_renewals`: Run daily, process due renewals
|
||||
- `woonoow_check_expired_subscriptions`: Mark expired subscriptions
|
||||
|
||||
---
|
||||
|
||||
## REST API Endpoints
|
||||
|
||||
### Admin Endpoints
|
||||
```
|
||||
GET /subscriptions # List all subscriptions
|
||||
GET /subscriptions/{id} # Get subscription details
|
||||
PUT /subscriptions/{id} # Update subscription
|
||||
POST /subscriptions/{id}/cancel # Cancel subscription
|
||||
POST /subscriptions/{id}/renew # Force renewal
|
||||
```
|
||||
|
||||
### Customer Endpoints
|
||||
```
|
||||
GET /my-subscriptions # Customer's subscriptions
|
||||
GET /my-subscriptions/{id} # Subscription detail
|
||||
POST /my-subscriptions/{id}/cancel # Request cancellation
|
||||
POST /my-subscriptions/{id}/pause # Pause subscription
|
||||
POST /my-subscriptions/{id}/resume # Resume subscription
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Admin SPA
|
||||
|
||||
### Subscriptions List (`/subscriptions`)
|
||||
- Table: ID, Customer, Product, Status, Next Payment, Actions
|
||||
- Filters: Status, Product, Date range
|
||||
- Actions: View, Cancel, Renew
|
||||
|
||||
### Subscription Detail (`/subscriptions/:id`)
|
||||
- Subscription info card
|
||||
- Related orders list
|
||||
- Payment history
|
||||
- Action buttons: Cancel, Pause, Renew
|
||||
|
||||
---
|
||||
|
||||
## Customer SPA
|
||||
|
||||
### My Subscriptions (`/my-account/subscriptions`)
|
||||
- List of active/past subscriptions
|
||||
- Status badges
|
||||
- Next payment info
|
||||
- Actions: Cancel, Pause, View
|
||||
|
||||
### Subscription Detail
|
||||
- Product info
|
||||
- Billing schedule
|
||||
- Payment history
|
||||
- Management actions
|
||||
|
||||
---
|
||||
|
||||
## Settings Schema
|
||||
|
||||
```php
|
||||
return [
|
||||
'default_status' => 'active',
|
||||
'button_text_subscribe' => 'Subscribe Now',
|
||||
'button_text_renew' => 'Renew Subscription',
|
||||
'allow_customer_cancel' => true,
|
||||
'allow_customer_pause' => true,
|
||||
'max_pause_count' => 3,
|
||||
'renewal_retry_days' => [1, 3, 5], // Retry failed payments
|
||||
'expire_after_failed_attempts' => 3,
|
||||
'send_renewal_reminder' => true,
|
||||
'reminder_days_before' => 3,
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Priority
|
||||
1. Database tables and SubscriptionManager
|
||||
2. Product meta fields for subscription settings
|
||||
3. Order hook to create subscription
|
||||
4. Renewal cron job
|
||||
5. Admin SPA list/detail pages
|
||||
6. Customer SPA pages
|
||||
7. Integration with Licensing module
|
||||
@@ -34,6 +34,7 @@
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
@@ -63,9 +64,22 @@
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* { @apply border-border; }
|
||||
body { @apply bg-background text-foreground; }
|
||||
h1, h2, h3, h4, h5, h6 { @apply text-foreground; }
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply text-foreground;
|
||||
}
|
||||
|
||||
/* Override WordPress common.css focus/active styles */
|
||||
a:focus,
|
||||
@@ -126,11 +140,14 @@
|
||||
|
||||
/* Page defaults for print */
|
||||
@page {
|
||||
size: auto; /* let the browser choose */
|
||||
margin: 12mm; /* comfortable default */
|
||||
size: auto;
|
||||
/* let the browser choose */
|
||||
margin: 12mm;
|
||||
/* comfortable default */
|
||||
}
|
||||
|
||||
@media print {
|
||||
|
||||
/* Hide WordPress admin chrome */
|
||||
#adminmenuback,
|
||||
#adminmenuwrap,
|
||||
@@ -139,44 +156,124 @@
|
||||
#wpfooter,
|
||||
#screen-meta,
|
||||
.notice,
|
||||
.update-nag { display: none !important; }
|
||||
.update-nag {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Reset layout to full-bleed for our app */
|
||||
html, body, #wpwrap, #wpcontent { background: #fff !important; margin: 0 !important; padding: 0 !important; }
|
||||
#woonoow-admin-app, #woonoow-admin-app > div { margin: 0 !important; padding: 0 !important; max-width: 100% !important; }
|
||||
html,
|
||||
body,
|
||||
#wpwrap,
|
||||
#wpcontent {
|
||||
background: #fff !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#woonoow-admin-app,
|
||||
#woonoow-admin-app>div {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
/* Hide elements flagged as no-print, reveal print-only */
|
||||
.no-print { display: none !important; }
|
||||
.print-only { display: block !important; }
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.print-only {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Improve table row density on paper */
|
||||
.print-tight tr > * { padding-top: 6px !important; padding-bottom: 6px !important; }
|
||||
.print-tight tr>* {
|
||||
padding-top: 6px !important;
|
||||
padding-bottom: 6px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* By default, label-only content stays hidden unless in print or label mode */
|
||||
.print-only { display: none; }
|
||||
.print-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Label mode toggled by router (?mode=label) */
|
||||
.woonoow-label-mode .print-only { display: block; }
|
||||
.woonoow-label-mode .print-only {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.woonoow-label-mode .no-print-label,
|
||||
.woonoow-label-mode .wp-header-end,
|
||||
.woonoow-label-mode .wrap { display: none !important; }
|
||||
.woonoow-label-mode .wrap {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Optional page presets (opt-in by adding the class to a wrapper before printing) */
|
||||
.print-a4 { }
|
||||
.print-letter { }
|
||||
.print-4x6 { }
|
||||
.print-a4 {}
|
||||
|
||||
.print-letter {}
|
||||
|
||||
.print-4x6 {}
|
||||
|
||||
@media print {
|
||||
.print-a4 { }
|
||||
.print-letter { }
|
||||
|
||||
/* A4 Invoice layout */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.print-a4 {
|
||||
width: 210mm !important;
|
||||
min-height: 297mm !important;
|
||||
padding: 20mm !important;
|
||||
margin: 0 auto !important;
|
||||
box-sizing: border-box !important;
|
||||
background: white !important;
|
||||
-webkit-print-color-adjust: exact !important;
|
||||
print-color-adjust: exact !important;
|
||||
}
|
||||
|
||||
.print-a4 * {
|
||||
-webkit-print-color-adjust: exact !important;
|
||||
print-color-adjust: exact !important;
|
||||
}
|
||||
|
||||
/* Ensure backgrounds print */
|
||||
.print-a4 .bg-gray-50 {
|
||||
background-color: #f9fafb !important;
|
||||
}
|
||||
|
||||
.print-a4 .bg-gray-900 {
|
||||
background-color: #111827 !important;
|
||||
}
|
||||
|
||||
.print-a4 .text-white {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.print-letter {}
|
||||
|
||||
/* Thermal label (4x6in) with minimal margins */
|
||||
.print-4x6 { width: 6in; }
|
||||
.print-4x6 * { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.print-4x6 {
|
||||
width: 6in;
|
||||
}
|
||||
|
||||
.print-4x6 * {
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- WooNooW: Popper menus & fullscreen fixes --- */
|
||||
[data-radix-popper-content-wrapper] { z-index: 2147483647 !important; }
|
||||
body.woonoow-fullscreen .woonoow-app { overflow: visible; }
|
||||
[data-radix-popper-content-wrapper] {
|
||||
z-index: 2147483647 !important;
|
||||
}
|
||||
|
||||
body.woonoow-fullscreen .woonoow-app {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* --- WooCommerce Admin Notices --- */
|
||||
.woocommerce-message,
|
||||
|
||||
@@ -205,9 +205,11 @@ export default function OrderShow() {
|
||||
<button className="border rounded-md px-3 py-2 text-sm flex items-center gap-2 no-print" onClick={printInvoice} title={__('Print invoice')}>
|
||||
<FileText className="w-4 h-4" /> {__('Invoice')}
|
||||
</button>
|
||||
{!isVirtualOnly && (
|
||||
<button className="border rounded-md px-3 py-2 text-sm flex items-center gap-2 no-print" onClick={printLabel} title={__('Print shipping label')}>
|
||||
<Ticket className="w-4 h-4" /> {__('Label')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -475,54 +477,115 @@ export default function OrderShow() {
|
||||
{order && (
|
||||
<div className="print-only">
|
||||
{mode === 'invoice' && (
|
||||
<div className="max-w-[800px] mx-auto p-6 text-sm">
|
||||
<div className="flex items-start justify-between mb-6">
|
||||
<div className="print-a4 bg-white" style={{ minHeight: '297mm', width: '210mm', margin: '0 auto', padding: '20mm', boxSizing: 'border-box' }}>
|
||||
{/* Invoice Header */}
|
||||
<div className="flex items-start justify-between mb-8 pb-6 border-b-2 border-gray-200">
|
||||
<div>
|
||||
<div className="text-xl font-semibold">Invoice</div>
|
||||
<div className="opacity-60">Order #{order.number} · {new Date((order.date_ts || 0) * 1000).toLocaleString()}</div>
|
||||
<div className="text-3xl font-bold text-gray-900">{__('INVOICE')}</div>
|
||||
<div className="text-sm text-gray-600 mt-1">#{order.number}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-medium">{siteTitle}</div>
|
||||
<div className="opacity-60 text-xs">{window.location.origin}</div>
|
||||
<div className="text-xl font-semibold text-gray-900">{siteTitle}</div>
|
||||
<div className="text-sm text-gray-500 mt-1">{window.location.origin}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-6 mb-6">
|
||||
|
||||
{/* Invoice Meta */}
|
||||
<div className="grid grid-cols-2 gap-8 mb-8">
|
||||
<div>
|
||||
<div className="text-xs opacity-60 mb-1">{__('Bill To')}</div>
|
||||
<div className="text-sm" dangerouslySetInnerHTML={{ __html: order.billing?.address || order.billing?.name || '' }} />
|
||||
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">{__('Invoice Date')}</div>
|
||||
<div className="text-sm text-gray-900">{new Date((order.date_ts || 0) * 1000).toLocaleDateString()}</div>
|
||||
{order.payment_method && (
|
||||
<>
|
||||
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2 mt-4">{__('Payment Method')}</div>
|
||||
<div className="text-sm text-gray-900">{order.payment_method}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<canvas ref={qrRef} className="inline-block w-24 h-24 border" />
|
||||
<div className="flex justify-end">
|
||||
<canvas ref={qrRef} className="w-24 h-24" style={{ border: '1px solid #e5e7eb' }} />
|
||||
</div>
|
||||
</div>
|
||||
<table className="w-full border-collapse mb-6">
|
||||
|
||||
{/* Billing & Shipping */}
|
||||
<div className="grid grid-cols-2 gap-8 mb-8">
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">{__('Bill To')}</div>
|
||||
<div className="text-sm text-gray-900 font-medium">{order.billing?.name || '—'}</div>
|
||||
{order.billing?.email && <div className="text-sm text-gray-600">{order.billing.email}</div>}
|
||||
{order.billing?.phone && <div className="text-sm text-gray-600">{order.billing.phone}</div>}
|
||||
<div className="text-sm text-gray-600 mt-2" dangerouslySetInnerHTML={{ __html: order.billing?.address || '' }} />
|
||||
</div>
|
||||
{!isVirtualOnly && order.shipping?.name && (
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">{__('Ship To')}</div>
|
||||
<div className="text-sm text-gray-900 font-medium">{order.shipping?.name || '—'}</div>
|
||||
<div className="text-sm text-gray-600 mt-2" dangerouslySetInnerHTML={{ __html: order.shipping?.address || '' }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Items Table */}
|
||||
<table className="w-full mb-8" style={{ borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left border-b py-2 pr-2">Product</th>
|
||||
<th className="text-right border-b py-2 px-2">Qty</th>
|
||||
<th className="text-right border-b py-2 px-2">Subtotal</th>
|
||||
<th className="text-right border-b py-2 pl-2">Total</th>
|
||||
<tr className="bg-gray-900 text-white">
|
||||
<th className="text-left py-3 px-4 text-sm font-semibold">{__('Product')}</th>
|
||||
<th className="text-center py-3 px-4 text-sm font-semibold w-20">{__('Qty')}</th>
|
||||
<th className="text-right py-3 px-4 text-sm font-semibold w-32">{__('Price')}</th>
|
||||
<th className="text-right py-3 px-4 text-sm font-semibold w-32">{__('Total')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(order.items || []).map((it: any) => (
|
||||
<tr key={it.id}>
|
||||
<td className="py-1 pr-2">{it.name}</td>
|
||||
<td className="py-1 px-2 text-right">×{it.qty}</td>
|
||||
<td className="py-1 px-2 text-right"><Money value={it.subtotal} currency={order.currency} symbol={order.currency_symbol} /></td>
|
||||
<td className="py-1 pl-2 text-right"><Money value={it.total} currency={order.currency} symbol={order.currency_symbol} /></td>
|
||||
{(order.items || []).map((it: any, idx: number) => (
|
||||
<tr key={it.id} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||
<td className="py-3 px-4 text-sm">
|
||||
<div className="font-medium text-gray-900">{it.name}</div>
|
||||
{it.sku && <div className="text-xs text-gray-500">SKU: {it.sku}</div>}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm text-center text-gray-600">{it.qty}</td>
|
||||
<td className="py-3 px-4 text-sm text-right text-gray-600"><Money value={it.subtotal / it.qty} currency={order.currency} symbol={order.currency_symbol} /></td>
|
||||
<td className="py-3 px-4 text-sm text-right font-medium text-gray-900"><Money value={it.total} currency={order.currency} symbol={order.currency_symbol} /></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="flex justify-end">
|
||||
<div className="min-w-[260px]">
|
||||
<div className="flex justify-between"><span>Subtotal</span><span><Money value={order.totals?.subtotal} currency={order.currency} symbol={order.currency_symbol} /></span></div>
|
||||
<div className="flex justify-between"><span>Discount</span><span><Money value={order.totals?.discount} currency={order.currency} symbol={order.currency_symbol} /></span></div>
|
||||
<div className="flex justify-between"><span>Shipping</span><span><Money value={order.totals?.shipping} currency={order.currency} symbol={order.currency_symbol} /></span></div>
|
||||
<div className="flex justify-between"><span>Tax</span><span><Money value={order.totals?.tax} currency={order.currency} symbol={order.currency_symbol} /></span></div>
|
||||
<div className="flex justify-between font-semibold border-t mt-2 pt-2"><span>Total</span><span><Money value={order.totals?.total} currency={order.currency} symbol={order.currency_symbol} /></span></div>
|
||||
|
||||
{/* Totals */}
|
||||
<div className="flex justify-end mb-8">
|
||||
<div className="w-72">
|
||||
<div className="flex justify-between py-2 text-sm">
|
||||
<span className="text-gray-600">{__('Subtotal')}</span>
|
||||
<span className="text-gray-900"><Money value={order.totals?.subtotal} currency={order.currency} symbol={order.currency_symbol} /></span>
|
||||
</div>
|
||||
{(order.totals?.discount || 0) > 0 && (
|
||||
<div className="flex justify-between py-2 text-sm">
|
||||
<span className="text-gray-600">{__('Discount')}</span>
|
||||
<span className="text-green-600">-<Money value={order.totals?.discount} currency={order.currency} symbol={order.currency_symbol} /></span>
|
||||
</div>
|
||||
)}
|
||||
{(order.totals?.shipping || 0) > 0 && (
|
||||
<div className="flex justify-between py-2 text-sm">
|
||||
<span className="text-gray-600">{__('Shipping')}</span>
|
||||
<span className="text-gray-900"><Money value={order.totals?.shipping} currency={order.currency} symbol={order.currency_symbol} /></span>
|
||||
</div>
|
||||
)}
|
||||
{(order.totals?.tax || 0) > 0 && (
|
||||
<div className="flex justify-between py-2 text-sm">
|
||||
<span className="text-gray-600">{__('Tax')}</span>
|
||||
<span className="text-gray-900"><Money value={order.totals?.tax} currency={order.currency} symbol={order.currency_symbol} /></span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between py-3 mt-2 border-t-2 border-gray-900">
|
||||
<span className="text-lg font-bold text-gray-900">{__('Total')}</span>
|
||||
<span className="text-lg font-bold text-gray-900"><Money value={order.totals?.total} currency={order.currency} symbol={order.currency_symbol} /></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-auto pt-8 border-t border-gray-200 text-center text-xs text-gray-500">
|
||||
<p>{__('Thank you for your business!')}</p>
|
||||
<p className="mt-1">{siteTitle} • {window.location.origin}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user