Fixed 89 violations across vendor, admin, and shared JavaScript files: JS-008 (raw fetch → apiClient): - Added postFormData() and getBlob() methods to api-client.js - Updated inventory.js, messages.js to use apiClient.postFormData() - Added noqa for file downloads that need response headers JS-009 (window.showToast → Utils.showToast): - Updated admin/messages.js, notifications.js, vendor/messages.js - Replaced alert() in customers.js JS-006 (async error handling): - Added try/catch to all async init() and reload() methods - Fixed vendor: billing, dashboard, login, messages, onboarding - Fixed shared: feature-store, upgrade-prompts - Fixed admin: all page components JS-005 (init guards): - Added initialization guards to prevent duplicate init() calls - Pattern: if (window._componentInitialized) return; 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
218 lines
7.0 KiB
JavaScript
218 lines
7.0 KiB
JavaScript
// static/shared/js/feature-store.js
|
|
/**
|
|
* Feature Store for Alpine.js
|
|
*
|
|
* Provides feature availability checking for tier-based access control.
|
|
* Loads features from the API on init and caches them for the session.
|
|
*
|
|
* Usage in templates:
|
|
*
|
|
* 1. Check if feature is available:
|
|
* <div x-show="$store.features.has('analytics_dashboard')">
|
|
* Analytics content here
|
|
* </div>
|
|
*
|
|
* 2. Show upgrade prompt if not available:
|
|
* <div x-show="!$store.features.has('analytics_dashboard')">
|
|
* <p>Upgrade to access Analytics</p>
|
|
* </div>
|
|
*
|
|
* 3. Conditionally render with x-if:
|
|
* <template x-if="$store.features.has('api_access')">
|
|
* <a href="/settings/api">API Settings</a>
|
|
* </template>
|
|
*
|
|
* 4. Use feature data for upgrade prompts:
|
|
* <p x-text="$store.features.getUpgradeTier('analytics_dashboard')"></p>
|
|
*
|
|
* 5. Get current tier info:
|
|
* <span x-text="$store.features.tierName"></span>
|
|
*/
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
// Use centralized logger if available
|
|
const log = window.LogConfig?.log || console;
|
|
|
|
/**
|
|
* Feature Store
|
|
*/
|
|
const featureStore = {
|
|
// State
|
|
features: [], // Array of feature codes available to vendor
|
|
featuresMap: {}, // Full feature info keyed by code
|
|
tierCode: null, // Current tier code
|
|
tierName: null, // Current tier name
|
|
loading: true, // Loading state
|
|
loaded: false, // Whether features have been loaded
|
|
error: null, // Error message if load failed
|
|
|
|
/**
|
|
* Initialize the feature store
|
|
* Called automatically when Alpine starts
|
|
*/
|
|
async init() {
|
|
// Guard against multiple initialization
|
|
if (window._featureStoreInitialized) return;
|
|
window._featureStoreInitialized = true;
|
|
|
|
try {
|
|
log.debug('[FeatureStore] Initializing...');
|
|
await this.loadFeatures();
|
|
} catch (error) {
|
|
log.error('[FeatureStore] Failed to initialize:', error);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load features from API
|
|
*/
|
|
async loadFeatures() {
|
|
// Don't reload if already loaded
|
|
if (this.loaded) {
|
|
log.debug('[FeatureStore] Already loaded, skipping');
|
|
return;
|
|
}
|
|
|
|
// Get vendor code from URL
|
|
const vendorCode = this.getVendorCode();
|
|
if (!vendorCode) {
|
|
log.warn('[FeatureStore] No vendor code found in URL');
|
|
this.loading = false;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.loading = true;
|
|
this.error = null;
|
|
|
|
// Fetch available features (lightweight endpoint)
|
|
const response = await apiClient.get('/vendor/features/available');
|
|
|
|
this.features = response.features || [];
|
|
this.tierCode = response.tier_code;
|
|
this.tierName = response.tier_name;
|
|
this.loaded = true;
|
|
|
|
log.debug(`[FeatureStore] Loaded ${this.features.length} features for ${this.tierName} tier`);
|
|
|
|
} catch (error) {
|
|
log.error('[FeatureStore] Failed to load features:', error);
|
|
this.error = error.message || 'Failed to load features';
|
|
// Set empty array so checks don't fail
|
|
this.features = [];
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load full feature details (with metadata)
|
|
* Use this when you need upgrade info
|
|
*/
|
|
async loadFullFeatures() {
|
|
const vendorCode = this.getVendorCode();
|
|
if (!vendorCode) return;
|
|
|
|
try {
|
|
const response = await apiClient.get('/vendor/features');
|
|
|
|
// Build map for quick lookup
|
|
this.featuresMap = {};
|
|
for (const feature of response.features) {
|
|
this.featuresMap[feature.code] = feature;
|
|
}
|
|
|
|
log.debug(`[FeatureStore] Loaded full details for ${response.features.length} features`);
|
|
|
|
} catch (error) {
|
|
log.error('[FeatureStore] Failed to load full features:', error);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Check if vendor has access to a feature
|
|
* @param {string} featureCode - The feature code to check
|
|
* @returns {boolean} - Whether the feature is available
|
|
*/
|
|
has(featureCode) {
|
|
return this.features.includes(featureCode);
|
|
},
|
|
|
|
/**
|
|
* Check if vendor has access to ANY of the given features
|
|
* @param {...string} featureCodes - Feature codes to check
|
|
* @returns {boolean} - Whether any feature is available
|
|
*/
|
|
hasAny(...featureCodes) {
|
|
return featureCodes.some(code => this.has(code));
|
|
},
|
|
|
|
/**
|
|
* Check if vendor has access to ALL of the given features
|
|
* @param {...string} featureCodes - Feature codes to check
|
|
* @returns {boolean} - Whether all features are available
|
|
*/
|
|
hasAll(...featureCodes) {
|
|
return featureCodes.every(code => this.has(code));
|
|
},
|
|
|
|
/**
|
|
* Get feature info (requires loadFullFeatures first)
|
|
* @param {string} featureCode - The feature code
|
|
* @returns {object|null} - Feature info or null
|
|
*/
|
|
getFeature(featureCode) {
|
|
return this.featuresMap[featureCode] || null;
|
|
},
|
|
|
|
/**
|
|
* Get the tier name required for a feature
|
|
* @param {string} featureCode - The feature code
|
|
* @returns {string|null} - Tier name or null
|
|
*/
|
|
getUpgradeTier(featureCode) {
|
|
const feature = this.getFeature(featureCode);
|
|
return feature?.minimum_tier_name || null;
|
|
},
|
|
|
|
/**
|
|
* Get vendor code from URL
|
|
* @returns {string|null}
|
|
*/
|
|
getVendorCode() {
|
|
const path = window.location.pathname;
|
|
const segments = path.split('/').filter(Boolean);
|
|
if (segments[0] === 'vendor' && segments[1]) {
|
|
return segments[1];
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Reload features (e.g., after tier change)
|
|
*/
|
|
async reload() {
|
|
try {
|
|
this.loaded = false;
|
|
this.features = [];
|
|
this.featuresMap = {};
|
|
await this.loadFeatures();
|
|
} catch (error) {
|
|
log.error('[FeatureStore] Failed to reload:', error);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Register Alpine store when Alpine is available
|
|
document.addEventListener('alpine:init', () => {
|
|
Alpine.store('features', featureStore);
|
|
log.debug('[FeatureStore] Registered as Alpine store');
|
|
});
|
|
|
|
// Also expose globally for non-Alpine usage
|
|
window.FeatureStore = featureStore;
|
|
|
|
})();
|