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:
@@ -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"
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -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 [],
|
||||||
|
|||||||
176
app/services/platform_settings_service.py
Normal file
176
app/services/platform_settings_service.py
Normal 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()
|
||||||
@@ -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 '-';
|
||||||
|
|||||||
@@ -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 '-';
|
||||||
|
|||||||
@@ -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 '-';
|
||||||
|
|||||||
@@ -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 #}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
# ========================================================================
|
# ========================================================================
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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...")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
6
static/vendor/js/analytics.js
vendored
6
static/vendor/js/analytics.js
vendored
@@ -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) {
|
||||||
|
|||||||
6
static/vendor/js/billing.js
vendored
6
static/vendor/js/billing.js
vendored
@@ -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);
|
||||||
|
|||||||
6
static/vendor/js/content-page-edit.js
vendored
6
static/vendor/js/content-page-edit.js
vendored
@@ -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);
|
||||||
|
|||||||
6
static/vendor/js/content-pages.js
vendored
6
static/vendor/js/content-pages.js
vendored
@@ -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 ===');
|
||||||
|
|||||||
6
static/vendor/js/customers.js
vendored
6
static/vendor/js/customers.js
vendored
@@ -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();
|
||||||
|
|||||||
6
static/vendor/js/inventory.js
vendored
6
static/vendor/js/inventory.js
vendored
@@ -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();
|
||||||
|
|||||||
6
static/vendor/js/messages.js
vendored
6
static/vendor/js/messages.js
vendored
@@ -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([
|
||||||
|
|||||||
6
static/vendor/js/notifications.js
vendored
6
static/vendor/js/notifications.js
vendored
@@ -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) {
|
||||||
|
|||||||
6
static/vendor/js/order-detail.js
vendored
6
static/vendor/js/order-detail.js
vendored
@@ -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;
|
||||||
|
|||||||
6
static/vendor/js/orders.js
vendored
6
static/vendor/js/orders.js
vendored
@@ -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();
|
||||||
|
|||||||
6
static/vendor/js/products.js
vendored
6
static/vendor/js/products.js
vendored
@@ -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();
|
||||||
|
|||||||
6
static/vendor/js/profile.js
vendored
6
static/vendor/js/profile.js
vendored
@@ -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) {
|
||||||
|
|||||||
6
static/vendor/js/settings.js
vendored
6
static/vendor/js/settings.js
vendored
@@ -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) {
|
||||||
|
|||||||
6
static/vendor/js/team.js
vendored
6
static/vendor/js/team.js
vendored
@@ -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(),
|
||||||
|
|||||||
Reference in New Issue
Block a user