Store detail page now shows all platform subscriptions instead of always "No Subscription Found". Subscriptions listing page renamed from Store to Merchant throughout (template, JS, menu, i18n) with Platform column added. Tiers API supports platform_id filtering. Merchant detail page no longer hardcodes 'oms' platform — loads all platforms, shows subscription cards per platform with labels, and the Create Subscription modal includes a platform selector with platform-filtered tiers. Create button always accessible in Quick Actions. Edit modal on /admin/subscriptions loads tiers from API filtered by platform instead of hardcoded options, sends tier_code (not tier) to match PATCH schema, and shows platform context. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
290 lines
12 KiB
JavaScript
290 lines
12 KiB
JavaScript
// noqa: js-006 - async init pattern is safe, loadData has try/catch
|
|
// static/admin/js/merchant-detail.js
|
|
|
|
// Create custom logger for merchant detail
|
|
const merchantDetailLog = window.LogConfig.createLogger('MERCHANT-DETAIL');
|
|
|
|
function adminMerchantDetail() {
|
|
return {
|
|
// Inherit base layout functionality from init-alpine.js
|
|
...data(),
|
|
|
|
// Merchant detail page specific state
|
|
currentPage: 'merchant-detail',
|
|
merchant: null,
|
|
loading: false,
|
|
error: null,
|
|
merchantId: null,
|
|
|
|
// Subscription state
|
|
platforms: [],
|
|
subscriptions: [],
|
|
tiers: [],
|
|
tiersForPlatformId: null,
|
|
showCreateSubscriptionModal: false,
|
|
createForm: { platform_id: null, tier_code: 'essential', status: 'trial', trial_days: 14, is_annual: false },
|
|
creatingSubscription: false,
|
|
|
|
// Initialize
|
|
async init() {
|
|
// Load i18n translations
|
|
await I18n.loadModule('tenancy');
|
|
|
|
merchantDetailLog.info('=== MERCHANT DETAIL PAGE INITIALIZING ===');
|
|
|
|
// Prevent multiple initializations
|
|
if (window._merchantDetailInitialized) {
|
|
merchantDetailLog.warn('Merchant detail page already initialized, skipping...');
|
|
return;
|
|
}
|
|
window._merchantDetailInitialized = true;
|
|
|
|
// Get merchant ID from URL
|
|
const path = window.location.pathname;
|
|
const match = path.match(/\/admin\/merchants\/(\d+)$/);
|
|
|
|
if (match) {
|
|
this.merchantId = match[1];
|
|
merchantDetailLog.info('Viewing merchant:', this.merchantId);
|
|
await this.loadMerchant();
|
|
await this.loadPlatforms();
|
|
await this.loadSubscriptions();
|
|
} else {
|
|
merchantDetailLog.error('No merchant ID in URL');
|
|
this.error = 'Invalid merchant URL';
|
|
Utils.showToast(I18n.t('tenancy.messages.invalid_merchant_url'), 'error');
|
|
}
|
|
|
|
merchantDetailLog.info('=== MERCHANT DETAIL PAGE INITIALIZATION COMPLETE ===');
|
|
},
|
|
|
|
// Load merchant data
|
|
async loadMerchant() {
|
|
merchantDetailLog.info('Loading merchant details...');
|
|
this.loading = true;
|
|
this.error = null;
|
|
|
|
try {
|
|
const url = `/admin/merchants/${this.merchantId}`;
|
|
window.LogConfig.logApiCall('GET', url, null, 'request');
|
|
|
|
const startTime = performance.now();
|
|
const response = await apiClient.get(url);
|
|
const duration = performance.now() - startTime;
|
|
|
|
window.LogConfig.logApiCall('GET', url, response, 'response');
|
|
window.LogConfig.logPerformance('Load Merchant Details', duration);
|
|
|
|
this.merchant = response;
|
|
|
|
merchantDetailLog.info(`Merchant loaded in ${duration}ms`, {
|
|
id: this.merchant.id,
|
|
name: this.merchant.name,
|
|
is_verified: this.merchant.is_verified,
|
|
is_active: this.merchant.is_active,
|
|
store_count: this.merchant.store_count
|
|
});
|
|
merchantDetailLog.debug('Full merchant data:', this.merchant);
|
|
|
|
} catch (error) {
|
|
window.LogConfig.logError(error, 'Load Merchant Details');
|
|
this.error = error.message || 'Failed to load merchant details';
|
|
Utils.showToast(I18n.t('tenancy.messages.failed_to_load_merchant_details'), 'error');
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
// Load all available platforms
|
|
async loadPlatforms() {
|
|
try {
|
|
const response = await apiClient.get('/admin/platforms');
|
|
this.platforms = (response.platforms || []).map(p => ({ id: p.id, name: p.name, code: p.code }));
|
|
merchantDetailLog.info('Platforms loaded:', this.platforms.length);
|
|
} catch (error) {
|
|
merchantDetailLog.warn('Failed to load platforms:', error.message);
|
|
}
|
|
},
|
|
|
|
// Load subscriptions for all platforms
|
|
async loadSubscriptions() {
|
|
if (!this.merchantId || this.platforms.length === 0) return;
|
|
|
|
merchantDetailLog.info('Loading subscriptions for merchant:', this.merchantId);
|
|
this.subscriptions = [];
|
|
|
|
for (const platform of this.platforms) {
|
|
try {
|
|
const url = `/admin/subscriptions/merchants/${this.merchantId}/platforms/${platform.id}`;
|
|
const response = await apiClient.get(url);
|
|
const sub = response.subscription || response;
|
|
|
|
this.subscriptions.push({
|
|
subscription: sub,
|
|
tier: response.tier || null,
|
|
features: response.features || [],
|
|
platform_id: platform.id,
|
|
platform_name: platform.name,
|
|
});
|
|
} catch (error) {
|
|
if (error.status !== 404) {
|
|
merchantDetailLog.warn(`Failed to load subscription for platform ${platform.name}:`, error.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
merchantDetailLog.info('Subscriptions loaded:', {
|
|
count: this.subscriptions.length,
|
|
platforms: this.subscriptions.map(e => e.platform_name)
|
|
});
|
|
},
|
|
|
|
// Load available subscription tiers for a platform
|
|
async loadTiers(platformId) {
|
|
if (this.tiers.length > 0 && this.tiersForPlatformId === platformId) return;
|
|
|
|
try {
|
|
const url = platformId
|
|
? `/admin/subscriptions/tiers?platform_id=${platformId}`
|
|
: '/admin/subscriptions/tiers';
|
|
const response = await apiClient.get(url);
|
|
this.tiers = response.tiers || [];
|
|
this.tiersForPlatformId = platformId;
|
|
merchantDetailLog.info('Loaded tiers:', this.tiers.length);
|
|
} catch (error) {
|
|
merchantDetailLog.warn('Failed to load tiers:', error.message);
|
|
}
|
|
},
|
|
|
|
// Open create subscription modal
|
|
async openCreateSubscriptionModal() {
|
|
const firstPlatformId = this.platforms.length > 0 ? this.platforms[0].id : null;
|
|
this.createForm = { platform_id: firstPlatformId, tier_code: 'essential', status: 'trial', trial_days: 14, is_annual: false };
|
|
await this.loadTiers(firstPlatformId);
|
|
this.showCreateSubscriptionModal = true;
|
|
},
|
|
|
|
// Reload tiers when platform changes in create modal
|
|
async onCreatePlatformChange() {
|
|
this.tiers = [];
|
|
this.tiersForPlatformId = null;
|
|
await this.loadTiers(this.createForm.platform_id);
|
|
},
|
|
|
|
// Create subscription for this merchant
|
|
async createSubscription() {
|
|
if (!this.merchantId || !this.createForm.platform_id) return;
|
|
|
|
this.creatingSubscription = true;
|
|
const platformId = this.createForm.platform_id;
|
|
merchantDetailLog.info('Creating subscription for merchant:', this.merchantId, 'platform:', platformId);
|
|
|
|
try {
|
|
const url = `/admin/subscriptions/merchants/${this.merchantId}/platforms/${platformId}`;
|
|
const payload = {
|
|
merchant_id: parseInt(this.merchantId),
|
|
platform_id: platformId,
|
|
tier_code: this.createForm.tier_code,
|
|
status: this.createForm.status,
|
|
trial_days: this.createForm.status === 'trial' ? parseInt(this.createForm.trial_days) : 0,
|
|
is_annual: this.createForm.is_annual
|
|
};
|
|
|
|
window.LogConfig.logApiCall('POST', url, payload, 'request');
|
|
const response = await apiClient.post(url, payload);
|
|
window.LogConfig.logApiCall('POST', url, response, 'response');
|
|
|
|
this.showCreateSubscriptionModal = false;
|
|
Utils.showToast('Subscription created successfully', 'success');
|
|
merchantDetailLog.info('Subscription created');
|
|
|
|
await this.loadSubscriptions();
|
|
|
|
} catch (error) {
|
|
window.LogConfig.logError(error, 'Create Subscription');
|
|
Utils.showToast(error.message || 'Failed to create subscription', 'error');
|
|
} finally {
|
|
this.creatingSubscription = false;
|
|
}
|
|
},
|
|
|
|
// Get usage bar color based on percentage
|
|
getUsageBarColor(current, limit) {
|
|
if (!limit || limit === 0) return 'bg-blue-500';
|
|
const percent = (current / limit) * 100;
|
|
if (percent >= 90) return 'bg-red-500';
|
|
if (percent >= 75) return 'bg-yellow-500';
|
|
return 'bg-green-500';
|
|
},
|
|
|
|
// Format tier price for display
|
|
formatTierPrice(tier) {
|
|
if (!tier.price_monthly_cents) return 'Custom';
|
|
return `€${(tier.price_monthly_cents / 100).toFixed(2)}/mo`;
|
|
},
|
|
|
|
// Format date (matches dashboard pattern)
|
|
formatDate(dateString) {
|
|
if (!dateString) {
|
|
merchantDetailLog.debug('formatDate called with empty dateString');
|
|
return '-';
|
|
}
|
|
const formatted = Utils.formatDate(dateString);
|
|
merchantDetailLog.debug(`Date formatted: ${dateString} -> ${formatted}`);
|
|
return formatted;
|
|
},
|
|
|
|
// Delete merchant
|
|
async deleteMerchant() {
|
|
merchantDetailLog.info('Delete merchant requested:', this.merchantId);
|
|
|
|
if (this.merchant?.store_count > 0) {
|
|
Utils.showToast(`Cannot delete merchant with ${this.merchant.store_count} store(s). Delete stores first.`, 'error');
|
|
return;
|
|
}
|
|
|
|
if (!confirm(`Are you sure you want to delete merchant "${this.merchant.name}"?\n\nThis action cannot be undone.`)) {
|
|
merchantDetailLog.info('Delete cancelled by user');
|
|
return;
|
|
}
|
|
|
|
// Second confirmation for safety
|
|
if (!confirm(`FINAL CONFIRMATION\n\nAre you absolutely sure you want to delete "${this.merchant.name}"?`)) {
|
|
merchantDetailLog.info('Delete cancelled by user (second confirmation)');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const url = `/admin/merchants/${this.merchantId}?confirm=true`;
|
|
window.LogConfig.logApiCall('DELETE', url, null, 'request');
|
|
|
|
merchantDetailLog.info('Deleting merchant:', this.merchantId);
|
|
await apiClient.delete(url);
|
|
|
|
window.LogConfig.logApiCall('DELETE', url, null, 'response');
|
|
|
|
Utils.showToast(I18n.t('tenancy.messages.merchant_deleted_successfully'), 'success');
|
|
merchantDetailLog.info('Merchant deleted successfully');
|
|
|
|
// Redirect to merchants list
|
|
setTimeout(() => window.location.href = '/admin/merchants', 1500);
|
|
|
|
} catch (error) {
|
|
window.LogConfig.logError(error, 'Delete Merchant');
|
|
Utils.showToast(error.message || 'Failed to delete merchant', 'error');
|
|
}
|
|
},
|
|
|
|
// Refresh merchant data
|
|
async refresh() {
|
|
merchantDetailLog.info('=== MERCHANT REFRESH TRIGGERED ===');
|
|
await this.loadMerchant();
|
|
await this.loadSubscriptions();
|
|
Utils.showToast(I18n.t('tenancy.messages.merchant_details_refreshed'), 'success');
|
|
merchantDetailLog.info('=== MERCHANT REFRESH COMPLETE ===');
|
|
}
|
|
};
|
|
}
|
|
|
|
merchantDetailLog.info('Merchant detail module loaded');
|