feat: add configurable currency locale and fix vendor JS init

Currency Locale Configuration:
- Add platform-level storefront settings (locale, currency)
- Create PlatformSettingsService with resolution chain:
  vendor → AdminSetting → environment → hardcoded fallback
- Add storefront_locale nullable field to Vendor model
- Update shop routes to resolve and pass locale to templates
- Add window.SHOP_CONFIG for frontend JavaScript access
- Centralize formatPrice() in shop-layout.js using SHOP_CONFIG
- Remove local formatPrice functions from shop templates

Vendor JS Bug Fix:
- Fix vendorCode being null on all vendor pages
- Root cause: page components overriding init() without calling parent
- Add parent init call to 14 vendor JS files
- Add JS-013 architecture rule to prevent future regressions
- Validator now checks vendor JS files for parent init pattern

Files changed:
- New: app/services/platform_settings_service.py
- New: alembic/versions/s7a8b9c0d1e2_add_storefront_locale_to_vendors.py
- Modified: 14 vendor JS files, shop templates, validation scripts

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-02 21:26:12 +01:00
parent d9d34ab102
commit c87bdfa129
30 changed files with 522 additions and 48 deletions

View File

@@ -77,6 +77,60 @@ javascript_rules:
file_pattern: "static/**/js/**/*.js" file_pattern: "static/**/js/**/*.js"
check: "async_error_handling" check: "async_error_handling"
- id: "JS-013"
name: "Components overriding init() must call parent init"
severity: "error"
description: |
When an Alpine.js component spreads ...data() and defines its own init() method,
it MUST call the parent init() first. The parent init() sets critical properties
like vendorCode (from URL), currentUser, and theme preference.
Without calling parent init(), properties like vendorCode will be null, causing
API calls like `/vendor/${this.vendorCode}/settings` to fail with
"Endpoint not found: /api/v1/vendor/null/settings".
WRONG (parent init never called):
function vendorSettings() {
return {
...data(),
async init() {
await this.loadSettings(); // this.vendorCode is null!
}
};
}
RIGHT (call parent init first):
function vendorSettings() {
return {
...data(),
async init() {
// IMPORTANT: Call parent init first to set vendorCode from URL
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
await this.loadSettings(); // this.vendorCode is now set
}
};
}
This pattern is required for ALL page-specific JavaScript files that:
1. Use ...data() to inherit base layout functionality
2. Define their own init() method
pattern:
file_pattern: "static/vendor/js/**/*.js"
check: "parent_init_call"
required_when:
- "contains: '...data()'"
- "contains: 'async init()'"
required_pattern:
- "data\\(\\)\\.init"
- "parentInit"
exceptions:
- "init-alpine.js"
- "login.js"
- id: "JS-007" - id: "JS-007"
name: "Set loading state before async operations" name: "Set loading state before async operations"
severity: "warning" severity: "warning"

View File

@@ -0,0 +1,40 @@
# alembic/versions/s7a8b9c0d1e2_add_storefront_locale_to_vendors.py
"""Add storefront_locale to vendors for currency formatting.
Revision ID: s7a8b9c0d1e2
Revises: r6f7a8b9c0d1
Create Date: 2026-01-02 20:00:00.000000
This migration adds a nullable storefront_locale field to vendors.
NULL means the vendor inherits from platform defaults.
Examples: 'fr-LU', 'de-DE', 'en-GB'
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "s7a8b9c0d1e2"
down_revision = "r6f7a8b9c0d1"
branch_labels = None
depends_on = None
def upgrade() -> None:
"""Add storefront_locale column to vendors table."""
# Nullable - NULL means "inherit from platform default"
op.add_column(
"vendors",
sa.Column(
"storefront_locale",
sa.String(10),
nullable=True,
comment="Currency/number formatting locale (NULL = inherit from platform)",
),
)
def downgrade() -> None:
"""Remove storefront_locale column from vendors table."""
op.drop_column("vendors", "storefront_locale")

View File

@@ -15,6 +15,7 @@ from app.api.deps import get_current_customer_api
from app.core.database import get_db from app.core.database import get_db
from app.exceptions import ValidationException from app.exceptions import ValidationException
from app.services.auth_service import AuthService from app.services.auth_service import AuthService
from app.services.customer_service import customer_service
from models.database.customer import Customer from models.database.customer import Customer
from models.schema.customer import ( from models.schema.customer import (
CustomerPasswordChange, CustomerPasswordChange,
@@ -81,16 +82,10 @@ def update_profile(
# If email is being changed, check uniqueness within vendor # If email is being changed, check uniqueness within vendor
if update_data.email and update_data.email != customer.email: if update_data.email and update_data.email != customer.email:
existing = ( existing = customer_service.get_customer_by_email(
db.query(Customer) db, customer.vendor_id, update_data.email
.filter(
Customer.vendor_id == customer.vendor_id,
Customer.email == update_data.email,
Customer.id != customer.id,
)
.first()
) )
if existing: if existing and existing.id != customer.id:
raise ValidationException("Email already in use") raise ValidationException("Email already in use")
# Update only provided fields # Update only provided fields

View File

@@ -158,6 +158,13 @@ class Settings(BaseSettings):
email_enabled: bool = True # Set to False to disable all emails email_enabled: bool = True # Set to False to disable all emails
email_debug: bool = False # Log emails instead of sending (for development) email_debug: bool = False # Log emails instead of sending (for development)
# =============================================================================
# STOREFRONT DEFAULTS
# =============================================================================
# These can be overridden by AdminSetting in the database
default_storefront_locale: str = "fr-LU" # Currency/number formatting locale
default_currency: str = "EUR" # Default currency code
# ============================================================================= # =============================================================================
# DEMO/SEED DATA CONFIGURATION # DEMO/SEED DATA CONFIGURATION
# ============================================================================= # =============================================================================

View File

@@ -39,6 +39,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_customer_from_cookie_or_header, get_db from app.api.deps import get_current_customer_from_cookie_or_header, get_db
from app.services.content_page_service import content_page_service from app.services.content_page_service import content_page_service
from app.services.platform_settings_service import platform_settings_service
from models.database.customer import Customer from models.database.customer import Customer
router = APIRouter() router = APIRouter()
@@ -47,6 +48,40 @@ templates = Jinja2Templates(directory="app/templates")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# ============================================================================
# HELPER: Resolve Storefront Locale
# ============================================================================
def get_resolved_storefront_config(db: Session, vendor) -> dict:
"""
Resolve storefront locale and currency with priority:
1. Vendor's storefront_locale (if set)
2. Platform's default_storefront_locale (from AdminSetting)
3. Environment variable (from config)
4. Hardcoded fallback: 'fr-LU'
Args:
db: Database session
vendor: Vendor model instance
Returns:
dict with 'locale' and 'currency' keys
"""
# Get platform defaults from service (handles resolution chain 2-4)
platform_config = platform_settings_service.get_storefront_config(db)
# Check for vendor override (step 1)
locale = platform_config["locale"]
if vendor and vendor.storefront_locale:
locale = vendor.storefront_locale
return {
"locale": locale,
"currency": platform_config["currency"],
}
# ============================================================================ # ============================================================================
# HELPER: Build Shop Template Context # HELPER: Build Shop Template Context
# ============================================================================ # ============================================================================
@@ -133,6 +168,11 @@ def get_shop_context(request: Request, db: Session = None, **extra_context) -> d
extra={"error": str(e), "vendor_id": vendor.id if vendor else None}, extra={"error": str(e), "vendor_id": vendor.id if vendor else None},
) )
# Resolve storefront locale and currency
storefront_config = {"locale": "fr-LU", "currency": "EUR"} # defaults
if db and vendor:
storefront_config = get_resolved_storefront_config(db, vendor)
context = { context = {
"request": request, "request": request,
"vendor": vendor, "vendor": vendor,
@@ -142,6 +182,8 @@ def get_shop_context(request: Request, db: Session = None, **extra_context) -> d
"base_url": base_url, "base_url": base_url,
"footer_pages": footer_pages, "footer_pages": footer_pages,
"header_pages": header_pages, "header_pages": header_pages,
"storefront_locale": storefront_config["locale"],
"storefront_currency": storefront_config["currency"],
} }
# Add any extra context (user, product_id, category_slug, etc.) # Add any extra context (user, product_id, category_slug, etc.)
@@ -157,6 +199,8 @@ def get_shop_context(request: Request, db: Session = None, **extra_context) -> d
"has_theme": theme is not None, "has_theme": theme is not None,
"access_method": access_method, "access_method": access_method,
"base_url": base_url, "base_url": base_url,
"storefront_locale": storefront_config["locale"],
"storefront_currency": storefront_config["currency"],
"footer_pages_count": len(footer_pages), "footer_pages_count": len(footer_pages),
"header_pages_count": len(header_pages), "header_pages_count": len(header_pages),
"extra_keys": list(extra_context.keys()) if extra_context else [], "extra_keys": list(extra_context.keys()) if extra_context else [],

View File

@@ -0,0 +1,176 @@
# app/services/platform_settings_service.py
"""
Platform Settings Service
Provides access to platform-wide settings with a resolution chain:
1. AdminSetting from database (can be set via admin UI)
2. Environment variables (from .env/config)
3. Hardcoded defaults
This allows admins to override defaults without code changes,
while still supporting environment-based configuration.
"""
import logging
from typing import Any
from sqlalchemy.orm import Session
from app.core.config import settings
from models.database.admin import AdminSetting
logger = logging.getLogger(__name__)
class PlatformSettingsService:
"""
Service for accessing platform-wide settings.
Resolution order:
1. AdminSetting in database (highest priority)
2. Environment variable via config
3. Hardcoded default (lowest priority)
"""
# Mapping of setting keys to their config attribute names and defaults
SETTINGS_MAP = {
"default_storefront_locale": {
"config_attr": "default_storefront_locale",
"default": "fr-LU",
"description": "Default locale for currency/number formatting (e.g., fr-LU, de-DE)",
"category": "storefront",
},
"default_currency": {
"config_attr": "default_currency",
"default": "EUR",
"description": "Default currency code for the platform",
"category": "storefront",
},
}
def get(self, db: Session, key: str) -> str | None:
"""
Get a setting value with full resolution chain.
Args:
db: Database session
key: Setting key (e.g., 'default_storefront_locale')
Returns:
Setting value or None if not found
"""
# 1. Check AdminSetting in database
admin_setting = db.query(AdminSetting).filter_by(key=key).first()
if admin_setting and admin_setting.value:
logger.debug(f"Setting '{key}' resolved from AdminSetting: {admin_setting.value}")
return admin_setting.value
# 2. Check environment/config
setting_info = self.SETTINGS_MAP.get(key)
if setting_info:
config_attr = setting_info.get("config_attr")
if config_attr and hasattr(settings, config_attr):
value = getattr(settings, config_attr)
logger.debug(f"Setting '{key}' resolved from config: {value}")
return value
# 3. Return hardcoded default
default = setting_info.get("default")
logger.debug(f"Setting '{key}' resolved from default: {default}")
return default
logger.warning(f"Unknown setting key: {key}")
return None
def get_storefront_locale(self, db: Session) -> str:
"""Get the default storefront locale."""
return self.get(db, "default_storefront_locale") or "fr-LU"
def get_currency(self, db: Session) -> str:
"""Get the default currency."""
return self.get(db, "default_currency") or "EUR"
def get_storefront_config(self, db: Session) -> dict[str, str]:
"""
Get all storefront-related settings as a dict.
Returns:
Dict with 'locale' and 'currency' keys
"""
return {
"locale": self.get_storefront_locale(db),
"currency": self.get_currency(db),
}
def set(self, db: Session, key: str, value: str, user_id: int | None = None) -> AdminSetting:
"""
Set a platform setting in the database.
Args:
db: Database session
key: Setting key
value: Setting value
user_id: ID of user making the change (for audit)
Returns:
The created/updated AdminSetting
"""
setting_info = self.SETTINGS_MAP.get(key, {})
admin_setting = db.query(AdminSetting).filter_by(key=key).first()
if admin_setting:
admin_setting.value = value
if user_id:
admin_setting.last_modified_by_user_id = user_id
else:
admin_setting = AdminSetting(
key=key,
value=value,
value_type="string",
category=setting_info.get("category", "system"),
description=setting_info.get("description", ""),
last_modified_by_user_id=user_id,
)
db.add(admin_setting)
db.commit() # noqa: SVC-006 - Setting change is atomic, commit is intentional
db.refresh(admin_setting)
logger.info(f"Platform setting '{key}' set to '{value}' by user {user_id}")
return admin_setting
def get_all_storefront_settings(self, db: Session) -> dict[str, Any]:
"""
Get all storefront settings with their current values and metadata.
Useful for admin UI to display current settings.
Returns:
Dict with setting info including current value and source
"""
result = {}
for key, info in self.SETTINGS_MAP.items():
if info.get("category") == "storefront":
current_value = self.get(db, key)
# Determine source
admin_setting = db.query(AdminSetting).filter_by(key=key).first()
if admin_setting and admin_setting.value:
source = "database"
elif hasattr(settings, info.get("config_attr", "")):
source = "environment"
else:
source = "default"
result[key] = {
"value": current_value,
"source": source,
"description": info.get("description", ""),
"default": info.get("default"),
}
return result
# Singleton instance
platform_settings_service = PlatformSettingsService()

View File

@@ -462,13 +462,7 @@ function shopOrderDetailPage() {
return this.statuses[status]?.class || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'; return this.statuses[status]?.class || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200';
}, },
formatPrice(amount) { // formatPrice is inherited from shopLayoutData() via spread operator
if (!amount && amount !== 0) return '-';
return new Intl.NumberFormat('fr-LU', {
style: 'currency',
currency: 'EUR'
}).format(amount);
},
formatDateTime(dateStr) { formatDateTime(dateStr) {
if (!dateStr) return '-'; if (!dateStr) return '-';

View File

@@ -217,13 +217,7 @@ function shopOrdersPage() {
return this.statuses[status]?.class || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'; return this.statuses[status]?.class || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200';
}, },
formatPrice(amount) { // formatPrice is inherited from shopLayoutData() via spread operator
if (!amount && amount !== 0) return '-';
return new Intl.NumberFormat('fr-LU', {
style: 'currency',
currency: 'EUR'
}).format(amount);
},
formatDate(dateStr) { formatDate(dateStr) {
if (!dateStr) return '-'; if (!dateStr) return '-';

View File

@@ -523,13 +523,7 @@ function shopProfilePage() {
} }
}, },
formatPrice(amount) { // formatPrice is inherited from shopLayoutData() via spread operator
if (!amount && amount !== 0) return '-';
return new Intl.NumberFormat('fr-LU', {
style: 'currency',
currency: 'EUR'
}).format(amount);
},
formatDate(dateStr) { formatDate(dateStr) {
if (!dateStr) return '-'; if (!dateStr) return '-';

View File

@@ -308,19 +308,28 @@
{# 1. Log Configuration (must load first) #} {# 1. Log Configuration (must load first) #}
<script src="{{ url_for('static', path='shared/js/log-config.js') }}"></script> <script src="{{ url_for('static', path='shared/js/log-config.js') }}"></script>
{# 2. Icon System #} {# 2. Global Shop Configuration (currency/locale settings) #}
<script>
window.SHOP_CONFIG = {
locale: '{{ storefront_locale | default("fr-LU") }}',
currency: '{{ storefront_currency | default("EUR") }}',
language: '{{ request.state.language|default("fr") }}'
};
</script>
{# 3. Icon System #}
<script src="{{ url_for('static', path='shared/js/icons.js') }}"></script> <script src="{{ url_for('static', path='shared/js/icons.js') }}"></script>
{# 3. Base Shop Layout (Alpine.js component - must load before Alpine) #} {# 4. Base Shop Layout (Alpine.js component - must load before Alpine) #}
<script src="{{ url_for('static', path='shop/js/shop-layout.js') }}"></script> <script src="{{ url_for('static', path='shop/js/shop-layout.js') }}"></script>
{# 4. Utilities #} {# 5. Utilities #}
<script src="{{ url_for('static', path='shared/js/utils.js') }}"></script> <script src="{{ url_for('static', path='shared/js/utils.js') }}"></script>
{# 5. API Client #} {# 6. API Client #}
<script src="{{ url_for('static', path='shared/js/api-client.js') }}"></script> <script src="{{ url_for('static', path='shared/js/api-client.js') }}"></script>
{# 6. Alpine.js with CDN fallback (deferred - loads last) #} {# 7. Alpine.js with CDN fallback (deferred - loads last) #}
<script> <script>
(function() { (function() {
var script = document.createElement('script'); var script = document.createElement('script');
@@ -339,7 +348,7 @@
})(); })();
</script> </script>
{# 7. Page-specific JavaScript #} {# 8. Page-specific JavaScript #}
{% block extra_scripts %}{% endblock %} {% block extra_scripts %}{% endblock %}
{# Toast notification container #} {# Toast notification container #}

View File

@@ -207,13 +207,7 @@ document.addEventListener('alpine:init', () => {
this.loadProducts(); this.loadProducts();
}, },
formatPrice(amount) { // formatPrice is inherited from shopLayoutData() via spread operator
if (!amount && amount !== 0) return '';
return new Intl.NumberFormat('fr-LU', {
style: 'currency',
currency: 'EUR'
}).format(amount);
},
async addToCart(product) { async addToCart(product) {
console.log('[SHOP] Adding to cart:', product); console.log('[SHOP] Adding to cart:', product);

View File

@@ -54,6 +54,7 @@ The OMS is nearly production ready with core order processing, invoicing, invent
| Customer profile | Complete | Full profile management | | Customer profile | Complete | Full profile management |
| Customer addresses | Complete | Multiple addresses, address book | | Customer addresses | Complete | Multiple addresses, address book |
| Customer messages | Complete | Conversation-based messaging | | Customer messages | Complete | Conversation-based messaging |
| Currency locale | Complete | Configurable platform/vendor locale |
### Vendor Dashboard (90% Complete) ### Vendor Dashboard (90% Complete)
@@ -123,6 +124,18 @@ The OMS is nearly production ready with core order processing, invoicing, invent
- Integration tests for shop addresses API - Integration tests for shop addresses API
- All validation scripts fixed and passing - All validation scripts fixed and passing
### Configurable Currency Locale
- **Two-tier settings architecture**: Platform defaults with vendor overrides
- **Resolution chain**: Vendor setting → AdminSetting → Environment → Hardcoded fallback
- **Platform settings service**: New `PlatformSettingsService` for setting resolution
- **Configuration options**:
- Environment: `DEFAULT_STOREFRONT_LOCALE`, `DEFAULT_CURRENCY` in `.env`
- Admin: `default_storefront_locale`, `default_currency` in AdminSetting table
- Vendor: `storefront_locale` field on Vendor model (nullable = inherit)
- **Supported locales**: fr-LU, de-DE, de-LU, en-GB, nl-BE, etc.
- **Frontend**: `window.SHOP_CONFIG` provides locale/currency to JavaScript
- **Shared formatPrice()**: Single implementation in `shop-layout.js`
--- ---
## Remaining Gaps ## Remaining Gaps

View File

@@ -121,6 +121,10 @@ class Vendor(Base, TimestampMixin):
JSON, nullable=False, default=["fr", "de", "en"] JSON, nullable=False, default=["fr", "de", "en"]
) # Array of enabled languages for storefront language selector ) # Array of enabled languages for storefront language selector
# Currency/number formatting locale (e.g., 'fr-LU' = "29,99 €", 'en-GB' = "€29.99")
# NULL means inherit from platform default (AdminSetting 'default_storefront_locale')
storefront_locale = Column(String(10), nullable=True)
# ======================================================================== # ========================================================================
# Relationships # Relationships
# ======================================================================== # ========================================================================

View File

@@ -81,6 +81,11 @@ class VendorCreate(BaseModel):
storefront_languages: list[str] | None = Field( storefront_languages: list[str] | None = Field(
default=["fr", "de", "en"], description="Enabled languages for storefront" default=["fr", "de", "en"], description="Enabled languages for storefront"
) )
storefront_locale: str | None = Field(
None,
description="Locale for currency/number formatting (e.g., 'fr-LU', 'de-DE'). NULL = inherit from platform default",
max_length=10,
)
@field_validator("subdomain") @field_validator("subdomain")
@classmethod @classmethod
@@ -152,6 +157,11 @@ class VendorUpdate(BaseModel):
storefront_languages: list[str] | None = Field( storefront_languages: list[str] | None = Field(
None, description="Enabled languages for storefront" None, description="Enabled languages for storefront"
) )
storefront_locale: str | None = Field(
None,
description="Locale for currency/number formatting (e.g., 'fr-LU', 'de-DE'). NULL = inherit from platform default",
max_length=10,
)
@field_validator("subdomain") @field_validator("subdomain")
@classmethod @classmethod
@@ -197,6 +207,9 @@ class VendorResponse(BaseModel):
storefront_language: str = "fr" storefront_language: str = "fr"
storefront_languages: list[str] = ["fr", "de", "en"] storefront_languages: list[str] = ["fr", "de", "en"]
# Currency/number formatting locale (NULL = inherit from platform default)
storefront_locale: str | None = None
# Timestamps # Timestamps
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime

View File

@@ -2862,6 +2862,9 @@ class ArchitectureValidator:
# JS-012: Check for double /api/v1 prefix in API calls # JS-012: Check for double /api/v1 prefix in API calls
self._check_api_prefix_usage(file_path, content, lines) self._check_api_prefix_usage(file_path, content, lines)
# JS-013: Check that components overriding init() call parent init
self._check_parent_init_call(file_path, content, lines)
def _check_platform_settings_usage( def _check_platform_settings_usage(
self, file_path: Path, content: str, lines: list[str] self, file_path: Path, content: str, lines: list[str]
): ):
@@ -2995,6 +2998,59 @@ class ArchitectureValidator:
suggestion="Change '/api/v1/admin/...' to '/admin/...'", suggestion="Change '/api/v1/admin/...' to '/admin/...'",
) )
def _check_parent_init_call(
self, file_path: Path, content: str, lines: list[str]
):
"""
JS-013: Check that vendor components overriding init() call parent init.
When a component uses ...data() to inherit base layout functionality AND
defines its own init() method, it MUST call the parent init first to set
critical properties like vendorCode.
Note: This only applies to vendor JS files because the vendor data() has
an init() method that extracts vendorCode from URL. Admin data() does not.
"""
# Only check vendor JS files (admin data() doesn't have init())
if "/vendor/js/" not in str(file_path):
return
# Skip files that shouldn't have this pattern
excluded_files = ["init-alpine.js", "login.js", "onboarding.js"]
if file_path.name in excluded_files:
return
# Check if file uses ...data() spread operator
uses_data_spread = "...data()" in content
# Check if file defines its own async init()
has_own_init = re.search(r"async\s+init\s*\(\s*\)", content)
# If both conditions are true, check for parent init call
if uses_data_spread and has_own_init:
# Check for parent init call patterns
calls_parent_init = (
"data().init" in content
or "parentInit" in content
or "parent.init" in content
)
if not calls_parent_init:
# Find the line with async init() to report
for i, line in enumerate(lines, 1):
if re.search(r"async\s+init\s*\(\s*\)", line):
self._add_violation(
rule_id="JS-013",
rule_name="Components overriding init() must call parent init",
severity=Severity.ERROR,
file_path=file_path,
line_number=i,
message="Component with ...data() must call parent init() to set vendorCode",
context=line.strip()[:80],
suggestion="Add: const parentInit = data().init; if (parentInit) { await parentInit.call(this); }",
)
break
def _validate_templates(self, target_path: Path): def _validate_templates(self, target_path: Path):
"""Validate template patterns""" """Validate template patterns"""
print("📄 Validating templates...") print("📄 Validating templates...")

View File

@@ -219,12 +219,15 @@ function shopLayoutData() {
}, 3000); }, 3000);
}, },
// Format currency // Format currency using configured locale
formatPrice(price) { formatPrice(amount) {
return new Intl.NumberFormat('en-US', { if (!amount && amount !== 0) return '';
const locale = window.SHOP_CONFIG?.locale || 'fr-LU';
const currency = window.SHOP_CONFIG?.currency || 'EUR';
return new Intl.NumberFormat(locale, {
style: 'currency', style: 'currency',
currency: 'USD' currency: currency
}).format(price); }).format(amount);
}, },
// Format date // Format date

View File

@@ -58,6 +58,12 @@ function vendorAnalytics() {
} }
window._vendorAnalyticsInitialized = true; window._vendorAnalyticsInitialized = true;
// IMPORTANT: Call parent init first to set vendorCode from URL
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
try { try {
await this.loadAllData(); await this.loadAllData();
} catch (error) { } catch (error) {

View File

@@ -33,6 +33,12 @@ function vendorBilling() {
if (window._vendorBillingInitialized) return; if (window._vendorBillingInitialized) return;
window._vendorBillingInitialized = true; window._vendorBillingInitialized = true;
// IMPORTANT: Call parent init first to set vendorCode from URL
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
try { try {
// Check URL params for success/cancel // Check URL params for success/cancel
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);

View File

@@ -44,6 +44,12 @@ function vendorContentPageEditor(pageId) {
} }
window._vendorContentPageEditInitialized = true; window._vendorContentPageEditInitialized = true;
// IMPORTANT: Call parent init first to set vendorCode from URL
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
try { try {
contentPageEditLog.info('=== VENDOR CONTENT PAGE EDITOR INITIALIZING ==='); contentPageEditLog.info('=== VENDOR CONTENT PAGE EDITOR INITIALIZING ===');
contentPageEditLog.info('Page ID:', this.pageId); contentPageEditLog.info('Page ID:', this.pageId);

View File

@@ -36,6 +36,12 @@ function vendorContentPagesManager() {
} }
window._vendorContentPagesInitialized = true; window._vendorContentPagesInitialized = true;
// IMPORTANT: Call parent init first to set vendorCode from URL
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
await this.loadPages(); await this.loadPages();
contentPagesLog.info('=== VENDOR CONTENT PAGES MANAGER INITIALIZATION COMPLETE ==='); contentPagesLog.info('=== VENDOR CONTENT PAGES MANAGER INITIALIZATION COMPLETE ===');

View File

@@ -106,6 +106,12 @@ function vendorCustomers() {
} }
window._vendorCustomersInitialized = true; window._vendorCustomersInitialized = true;
// IMPORTANT: Call parent init first to set vendorCode from URL
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
// Load platform settings for rows per page // Load platform settings for rows per page
if (window.PlatformSettings) { if (window.PlatformSettings) {
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage(); this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();

View File

@@ -137,6 +137,12 @@ function vendorInventory() {
} }
window._vendorInventoryInitialized = true; window._vendorInventoryInitialized = true;
// IMPORTANT: Call parent init first to set vendorCode from URL
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
// Load platform settings for rows per page // Load platform settings for rows per page
if (window.PlatformSettings) { if (window.PlatformSettings) {
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage(); this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();

View File

@@ -65,6 +65,12 @@ function vendorMessages(initialConversationId = null) {
if (window._vendorMessagesInitialized) return; if (window._vendorMessagesInitialized) return;
window._vendorMessagesInitialized = true; window._vendorMessagesInitialized = true;
// IMPORTANT: Call parent init first to set vendorCode from URL
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
try { try {
messagesLog.debug('Initializing vendor messages page'); messagesLog.debug('Initializing vendor messages page');
await Promise.all([ await Promise.all([

View File

@@ -60,6 +60,12 @@ function vendorNotifications() {
} }
window._vendorNotificationsInitialized = true; window._vendorNotificationsInitialized = true;
// IMPORTANT: Call parent init first to set vendorCode from URL
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
try { try {
await this.loadNotifications(); await this.loadNotifications();
} catch (error) { } catch (error) {

View File

@@ -62,6 +62,12 @@ function vendorOrderDetail() {
} }
window._orderDetailInitialized = true; window._orderDetailInitialized = true;
// IMPORTANT: Call parent init first to set vendorCode from URL
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
if (!this.orderId) { if (!this.orderId) {
this.error = 'Order ID not provided'; this.error = 'Order ID not provided';
this.loading = false; this.loading = false;

View File

@@ -137,6 +137,12 @@ function vendorOrders() {
} }
window._vendorOrdersInitialized = true; window._vendorOrdersInitialized = true;
// IMPORTANT: Call parent init first to set vendorCode from URL
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
// Load platform settings for rows per page // Load platform settings for rows per page
if (window.PlatformSettings) { if (window.PlatformSettings) {
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage(); this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();

View File

@@ -121,6 +121,12 @@ function vendorProducts() {
} }
window._vendorProductsInitialized = true; window._vendorProductsInitialized = true;
// IMPORTANT: Call parent init first to set vendorCode from URL
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
// Load platform settings for rows per page // Load platform settings for rows per page
if (window.PlatformSettings) { if (window.PlatformSettings) {
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage(); this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();

View File

@@ -54,6 +54,12 @@ function vendorProfile() {
} }
window._vendorProfileInitialized = true; window._vendorProfileInitialized = true;
// IMPORTANT: Call parent init first to set vendorCode from URL
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
try { try {
await this.loadProfile(); await this.loadProfile();
} catch (error) { } catch (error) {

View File

@@ -68,6 +68,12 @@ function vendorSettings() {
} }
window._vendorSettingsInitialized = true; window._vendorSettingsInitialized = true;
// IMPORTANT: Call parent init first to set vendorCode from URL
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
try { try {
await this.loadSettings(); await this.loadSettings();
} catch (error) { } catch (error) {

View File

@@ -73,6 +73,12 @@ function vendorTeam() {
} }
window._vendorTeamInitialized = true; window._vendorTeamInitialized = true;
// IMPORTANT: Call parent init first to set vendorCode from URL
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
try { try {
await Promise.all([ await Promise.all([
this.loadMembers(), this.loadMembers(),