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>
371 lines
12 KiB
JavaScript
371 lines
12 KiB
JavaScript
// noqa: js-006 - async init pattern is safe, loadData has try/catch
|
|
// static/admin/js/subscriptions.js
|
|
// noqa: JS-003 - Uses ...baseData which is data() with safety check
|
|
|
|
const subsLog = window.LogConfig?.loggers?.subscriptions || console;
|
|
|
|
function adminSubscriptions() {
|
|
// Get base data with safety check for standalone usage
|
|
const baseData = typeof data === 'function' ? data() : {};
|
|
|
|
return {
|
|
// Inherit base layout functionality from init-alpine.js
|
|
...baseData,
|
|
|
|
// Page-specific state
|
|
currentPage: 'subscriptions',
|
|
loading: true,
|
|
error: null,
|
|
successMessage: null,
|
|
saving: false,
|
|
|
|
// Data
|
|
subscriptions: [],
|
|
stats: null,
|
|
|
|
// Filters
|
|
filters: {
|
|
search: '',
|
|
status: '',
|
|
tier: ''
|
|
},
|
|
|
|
// Pagination
|
|
pagination: {
|
|
page: 1,
|
|
per_page: 20,
|
|
total: 0,
|
|
pages: 0
|
|
},
|
|
|
|
// Sorting
|
|
sortBy: 'merchant_name',
|
|
sortOrder: 'asc',
|
|
|
|
// Modal state
|
|
showModal: false,
|
|
editingSub: null,
|
|
formData: {
|
|
tier_code: '',
|
|
status: ''
|
|
},
|
|
|
|
// Tiers for edit modal
|
|
editTiers: [],
|
|
loadingTiers: false,
|
|
|
|
// Feature overrides
|
|
featureOverrides: [],
|
|
quantitativeFeatures: [],
|
|
loadingOverrides: false,
|
|
|
|
// Computed: Total pages
|
|
get totalPages() {
|
|
return this.pagination.pages;
|
|
},
|
|
|
|
// Computed: Start index for pagination display
|
|
get startIndex() {
|
|
if (this.pagination.total === 0) return 0;
|
|
return (this.pagination.page - 1) * this.pagination.per_page + 1;
|
|
},
|
|
|
|
// Computed: End index for pagination display
|
|
get endIndex() {
|
|
const end = this.pagination.page * this.pagination.per_page;
|
|
return end > this.pagination.total ? this.pagination.total : end;
|
|
},
|
|
|
|
// Computed: Page numbers for pagination
|
|
get pageNumbers() {
|
|
const pages = [];
|
|
const totalPages = this.totalPages;
|
|
const current = this.pagination.page;
|
|
|
|
if (totalPages <= 7) {
|
|
for (let i = 1; i <= totalPages; i++) {
|
|
pages.push(i);
|
|
}
|
|
} else {
|
|
pages.push(1);
|
|
if (current > 3) {
|
|
pages.push('...');
|
|
}
|
|
const start = Math.max(2, current - 1);
|
|
const end = Math.min(totalPages - 1, current + 1);
|
|
for (let i = start; i <= end; i++) {
|
|
pages.push(i);
|
|
}
|
|
if (current < totalPages - 2) {
|
|
pages.push('...');
|
|
}
|
|
pages.push(totalPages);
|
|
}
|
|
return pages;
|
|
},
|
|
|
|
async init() {
|
|
// Guard against multiple initialization
|
|
if (window._adminSubscriptionsInitialized) {
|
|
subsLog.warn('Already initialized, skipping');
|
|
return;
|
|
}
|
|
window._adminSubscriptionsInitialized = true;
|
|
|
|
subsLog.info('=== SUBSCRIPTIONS PAGE INITIALIZING ===');
|
|
|
|
// Load platform settings for rows per page
|
|
if (window.PlatformSettings) {
|
|
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
|
|
}
|
|
|
|
await this.loadStats();
|
|
await this.loadSubscriptions();
|
|
},
|
|
|
|
async refresh() {
|
|
this.error = null;
|
|
this.successMessage = null;
|
|
await this.loadStats();
|
|
await this.loadSubscriptions();
|
|
},
|
|
|
|
async loadStats() {
|
|
try {
|
|
const data = await apiClient.get('/admin/subscriptions/stats');
|
|
this.stats = data;
|
|
subsLog.info('Loaded subscription stats');
|
|
} catch (error) {
|
|
subsLog.error('Failed to load stats:', error);
|
|
}
|
|
},
|
|
|
|
async loadSubscriptions() {
|
|
this.loading = true;
|
|
this.error = null;
|
|
|
|
try {
|
|
const params = new URLSearchParams();
|
|
params.append('page', this.pagination.page);
|
|
params.append('per_page', this.pagination.per_page);
|
|
if (this.filters.status) params.append('status', this.filters.status);
|
|
if (this.filters.tier) params.append('tier', this.filters.tier);
|
|
if (this.filters.search) params.append('search', this.filters.search);
|
|
if (this.sortBy) params.append('sort_by', this.sortBy);
|
|
if (this.sortOrder) params.append('sort_order', this.sortOrder);
|
|
|
|
const data = await apiClient.get(`/admin/subscriptions?${params}`);
|
|
this.subscriptions = data.subscriptions || [];
|
|
this.pagination.total = data.total;
|
|
this.pagination.pages = data.pages;
|
|
subsLog.info(`Loaded ${this.subscriptions.length} subscriptions (total: ${this.pagination.total})`);
|
|
} catch (error) {
|
|
subsLog.error('Failed to load subscriptions:', error);
|
|
this.error = error.message || 'Failed to load subscriptions';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
resetFilters() {
|
|
this.filters = {
|
|
search: '',
|
|
status: '',
|
|
tier: ''
|
|
};
|
|
this.pagination.page = 1;
|
|
this.loadSubscriptions();
|
|
},
|
|
|
|
handleSort(key) {
|
|
if (this.sortBy === key) {
|
|
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
this.sortBy = key;
|
|
this.sortOrder = 'asc';
|
|
}
|
|
this.pagination.page = 1;
|
|
this.loadSubscriptions();
|
|
},
|
|
|
|
previousPage() {
|
|
if (this.pagination.page > 1) {
|
|
this.pagination.page--;
|
|
this.loadSubscriptions();
|
|
}
|
|
},
|
|
|
|
nextPage() {
|
|
if (this.pagination.page < this.totalPages) {
|
|
this.pagination.page++;
|
|
this.loadSubscriptions();
|
|
}
|
|
},
|
|
|
|
goToPage(pageNum) {
|
|
if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.totalPages) {
|
|
this.pagination.page = pageNum;
|
|
this.loadSubscriptions();
|
|
}
|
|
},
|
|
|
|
async openEditModal(sub) {
|
|
this.editingSub = sub;
|
|
this.formData = {
|
|
tier_code: sub.tier,
|
|
status: sub.status
|
|
};
|
|
this.featureOverrides = [];
|
|
this.quantitativeFeatures = [];
|
|
this.showModal = true;
|
|
|
|
// Load tiers filtered by platform and feature overrides in parallel
|
|
await Promise.all([
|
|
this.loadEditTiers(sub.platform_id),
|
|
this.loadFeatureOverrides(sub.merchant_id),
|
|
]);
|
|
},
|
|
|
|
async loadEditTiers(platformId) {
|
|
this.loadingTiers = true;
|
|
try {
|
|
const url = platformId
|
|
? `/admin/subscriptions/tiers?platform_id=${platformId}`
|
|
: '/admin/subscriptions/tiers';
|
|
const response = await apiClient.get(url);
|
|
this.editTiers = response.tiers || [];
|
|
subsLog.info('Loaded tiers for edit modal:', this.editTiers.length);
|
|
} catch (error) {
|
|
subsLog.error('Failed to load tiers:', error);
|
|
} finally {
|
|
this.loadingTiers = false;
|
|
}
|
|
},
|
|
|
|
closeModal() {
|
|
this.showModal = false;
|
|
this.editingSub = null;
|
|
},
|
|
|
|
async loadFeatureOverrides(merchantId) {
|
|
this.loadingOverrides = true;
|
|
try {
|
|
const [catalogData, overridesData] = await Promise.all([
|
|
apiClient.get('/admin/subscriptions/features/catalog'),
|
|
apiClient.get(`/admin/subscriptions/features/merchants/${merchantId}/overrides`),
|
|
]);
|
|
|
|
// Extract quantitative features from catalog
|
|
const allFeatures = [];
|
|
for (const [, features] of Object.entries(catalogData.features || {})) {
|
|
for (const f of features) {
|
|
if (f.feature_type === 'quantitative') {
|
|
allFeatures.push(f);
|
|
}
|
|
}
|
|
}
|
|
this.quantitativeFeatures = allFeatures;
|
|
|
|
// Map overrides by feature_code
|
|
this.featureOverrides = (overridesData || []).map(o => ({
|
|
feature_code: o.feature_code,
|
|
limit_value: o.limit_value,
|
|
is_enabled: o.is_enabled
|
|
}));
|
|
|
|
subsLog.info(`Loaded ${allFeatures.length} quantitative features and ${this.featureOverrides.length} overrides`);
|
|
} catch (error) {
|
|
subsLog.error('Failed to load feature overrides:', error);
|
|
} finally {
|
|
this.loadingOverrides = false;
|
|
}
|
|
},
|
|
|
|
getOverrideValue(featureCode) {
|
|
const override = this.featureOverrides.find(o => o.feature_code === featureCode);
|
|
return override?.limit_value ?? '';
|
|
},
|
|
|
|
setOverrideValue(featureCode, value) {
|
|
const numValue = value === '' ? null : parseInt(value, 10);
|
|
const existing = this.featureOverrides.find(o => o.feature_code === featureCode);
|
|
if (existing) {
|
|
existing.limit_value = numValue;
|
|
} else if (numValue !== null) {
|
|
this.featureOverrides.push({
|
|
feature_code: featureCode,
|
|
limit_value: numValue,
|
|
is_enabled: true
|
|
});
|
|
}
|
|
},
|
|
|
|
async saveFeatureOverrides(merchantId) {
|
|
// Only send overrides that have a limit_value set
|
|
const entries = this.featureOverrides
|
|
.filter(o => o.limit_value !== null && o.limit_value !== undefined)
|
|
.map(o => ({
|
|
feature_code: o.feature_code,
|
|
limit_value: o.limit_value,
|
|
is_enabled: true
|
|
}));
|
|
|
|
if (entries.length > 0) {
|
|
await apiClient.put(
|
|
`/admin/subscriptions/features/merchants/${merchantId}/overrides`,
|
|
entries
|
|
);
|
|
}
|
|
},
|
|
|
|
async saveSubscription() {
|
|
if (!this.editingSub) return;
|
|
|
|
this.saving = true;
|
|
this.error = null;
|
|
|
|
try {
|
|
const payload = { ...this.formData };
|
|
|
|
await apiClient.patch(
|
|
`/admin/subscriptions/merchants/${this.editingSub.merchant_id}/platforms/${this.editingSub.platform_id}`,
|
|
payload
|
|
);
|
|
|
|
// Save feature overrides
|
|
await this.saveFeatureOverrides(this.editingSub.merchant_id);
|
|
|
|
this.successMessage = `Subscription for "${this.editingSub.merchant_name}" updated`;
|
|
|
|
this.closeModal();
|
|
await this.loadSubscriptions();
|
|
await this.loadStats();
|
|
} catch (error) {
|
|
subsLog.error('Failed to save subscription:', error);
|
|
this.error = error.message || 'Failed to update subscription';
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
formatDate(dateStr) {
|
|
if (!dateStr) return '-';
|
|
return new Date(dateStr).toLocaleDateString('de-LU', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
});
|
|
},
|
|
|
|
formatCurrency(cents) {
|
|
if (cents === null || cents === undefined) return '-';
|
|
return new Intl.NumberFormat('de-LU', {
|
|
style: 'currency',
|
|
currency: 'EUR'
|
|
}).format(cents / 100);
|
|
}
|
|
};
|
|
}
|
|
|
|
subsLog.info('Subscriptions module loaded');
|