feat: consolidate media service, add merchant users page, fix metrics overlap
- Merge ImageService into MediaService with WebP variant generation, DB-backed storage stats, and module-driven media usage discovery via new MediaUsageProviderProtocol - Add merchant users admin page with scoped user listing, stats endpoint, template, JS, and i18n strings (de/en/fr/lb) - Fix merchant user metrics so Owners and Team Members are mutually exclusive (filter team_members on user_type="member" and exclude owner IDs) ensuring stat cards add up correctly - Update billing and monitoring services to use media_service - Update subscription-billing and feature-gating docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
230
app/modules/tenancy/static/admin/js/merchant-users.js
Normal file
230
app/modules/tenancy/static/admin/js/merchant-users.js
Normal file
@@ -0,0 +1,230 @@
|
||||
// noqa: js-006 - async init pattern is safe, loadData has try/catch
|
||||
// static/admin/js/merchant-users.js
|
||||
|
||||
// Create custom logger for merchant users
|
||||
const merchantUsersLog = window.LogConfig.createLogger('MERCHANT-USERS');
|
||||
|
||||
function merchantUsersPage() {
|
||||
return {
|
||||
// Inherit base layout functionality
|
||||
...data(),
|
||||
|
||||
// Set page identifier
|
||||
currentPage: 'merchant-users',
|
||||
|
||||
// State
|
||||
merchantUsers: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
filters: {
|
||||
search: '',
|
||||
is_active: ''
|
||||
},
|
||||
stats: {
|
||||
merchant_users_total: 0,
|
||||
merchant_owners: 0,
|
||||
merchant_team_members: 0,
|
||||
merchant_users_active: 0
|
||||
},
|
||||
pagination: {
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
pages: 0
|
||||
},
|
||||
|
||||
// Initialization
|
||||
async init() {
|
||||
// Load i18n translations
|
||||
await I18n.loadModule('tenancy');
|
||||
|
||||
merchantUsersLog.info('=== MERCHANT USERS PAGE INITIALIZING ===');
|
||||
|
||||
// Prevent multiple initializations
|
||||
if (window._merchantUsersInitialized) {
|
||||
merchantUsersLog.warn('Merchant users page already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
window._merchantUsersInitialized = true;
|
||||
|
||||
// Load platform settings for rows per page
|
||||
if (window.PlatformSettings) {
|
||||
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
|
||||
}
|
||||
|
||||
await this.loadMerchantUsers();
|
||||
await this.loadStats();
|
||||
|
||||
merchantUsersLog.info('=== MERCHANT USERS PAGE INITIALIZATION COMPLETE ===');
|
||||
},
|
||||
|
||||
// Format date helper
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
return Utils.formatDate(dateString);
|
||||
},
|
||||
|
||||
// Computed: Total number of 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: Generate page numbers array with ellipsis
|
||||
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;
|
||||
},
|
||||
|
||||
// Load merchant users from API
|
||||
async loadMerchantUsers() {
|
||||
merchantUsersLog.info('Loading merchant users...');
|
||||
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);
|
||||
params.append('scope', 'merchant');
|
||||
|
||||
if (this.filters.search) {
|
||||
params.append('search', this.filters.search);
|
||||
}
|
||||
if (this.filters.is_active !== '') {
|
||||
params.append('is_active', this.filters.is_active);
|
||||
}
|
||||
|
||||
const url = `/admin/users?${params}`;
|
||||
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 Users', duration);
|
||||
|
||||
this.merchantUsers = (response.items || []).map(user => ({
|
||||
...user,
|
||||
full_name: [user.first_name, user.last_name].filter(Boolean).join(' ') || null
|
||||
}));
|
||||
|
||||
this.pagination.total = response.total || 0;
|
||||
this.pagination.pages = response.pages || Math.ceil(this.pagination.total / this.pagination.per_page) || 1;
|
||||
|
||||
merchantUsersLog.info(`Loaded ${this.merchantUsers.length} merchant users`);
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Load Merchant Users');
|
||||
this.error = error.message || 'Failed to load merchant users';
|
||||
Utils.showToast('Failed to load merchant users', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Load statistics from metrics provider
|
||||
async loadStats() {
|
||||
merchantUsersLog.info('Loading merchant user statistics...');
|
||||
|
||||
try {
|
||||
const url = '/admin/users/merchant-stats';
|
||||
window.LogConfig.logApiCall('GET', url, null, 'request');
|
||||
|
||||
const response = await apiClient.get(url);
|
||||
|
||||
window.LogConfig.logApiCall('GET', url, response, 'response');
|
||||
|
||||
this.stats = {
|
||||
merchant_users_total: response.merchant_users_total || 0,
|
||||
merchant_owners: response.merchant_owners || 0,
|
||||
merchant_team_members: response.merchant_team_members || 0,
|
||||
merchant_users_active: response.merchant_users_active || 0
|
||||
};
|
||||
|
||||
merchantUsersLog.debug('Stats loaded:', this.stats);
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Load Merchant Stats');
|
||||
// Stats are non-critical, don't show error toast
|
||||
}
|
||||
},
|
||||
|
||||
// Search with debounce
|
||||
debouncedSearch() {
|
||||
if (this._searchTimeout) {
|
||||
clearTimeout(this._searchTimeout);
|
||||
}
|
||||
this._searchTimeout = setTimeout(() => {
|
||||
merchantUsersLog.info('Search triggered:', this.filters.search);
|
||||
this.pagination.page = 1;
|
||||
this.loadMerchantUsers();
|
||||
}, 300);
|
||||
},
|
||||
|
||||
// Pagination
|
||||
nextPage() {
|
||||
if (this.pagination.page < this.pagination.pages) {
|
||||
this.pagination.page++;
|
||||
merchantUsersLog.info('Next page:', this.pagination.page);
|
||||
this.loadMerchantUsers();
|
||||
}
|
||||
},
|
||||
|
||||
previousPage() {
|
||||
if (this.pagination.page > 1) {
|
||||
this.pagination.page--;
|
||||
merchantUsersLog.info('Previous page:', this.pagination.page);
|
||||
this.loadMerchantUsers();
|
||||
}
|
||||
},
|
||||
|
||||
goToPage(pageNum) {
|
||||
if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.totalPages) {
|
||||
this.pagination.page = pageNum;
|
||||
merchantUsersLog.info('Go to page:', this.pagination.page);
|
||||
this.loadMerchantUsers();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
merchantUsersLog.info('Merchant users module loaded');
|
||||
Reference in New Issue
Block a user