From 3cbe7e2979487d7c9540c9087dda60c7d950d06a Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Sat, 24 Jan 2026 19:49:40 +0100 Subject: [PATCH] feat: add platform assignment to user and vendor creation forms User create page: - When role=admin, show super admin toggle - If not super admin, show platform multi-select - Admin users created via /api/v1/admin/admin-users endpoint - Vendor users created via existing /admin/users endpoint Vendor create page: - Add platform selection section - Vendors can be assigned to multiple platforms on creation - Update VendorCreate schema to accept platform_ids - Update AdminService.create_vendor() to create VendorPlatform records Co-Authored-By: Claude Opus 4.5 --- app/services/admin_service.py | 19 +++++ app/templates/admin/user-create.html | 56 ++++++++++++- app/templates/admin/vendor-create.html | 38 +++++++++ models/schema/vendor.py | 5 ++ static/admin/js/user-create.js | 105 ++++++++++++++++++++++--- static/admin/js/vendor-create.js | 31 +++++++- 6 files changed, 241 insertions(+), 13 deletions(-) diff --git a/app/services/admin_service.py b/app/services/admin_service.py index 8a44cf9b..f016c4a5 100644 --- a/app/services/admin_service.py +++ b/app/services/admin_service.py @@ -34,6 +34,7 @@ from app.exceptions.auth import UserAlreadyExistsException from middleware.auth import AuthManager from models.database.company import Company from models.database.marketplace_import_job import MarketplaceImportJob +from models.database.platform import Platform from models.database.user import User from models.database.vendor import Role, Vendor from models.schema.marketplace_import_job import MarketplaceImportJobResponse @@ -419,6 +420,24 @@ class AdminService: # Create default roles for vendor self._create_default_roles(db, vendor.id) + # Assign vendor to platforms if provided + if vendor_data.platform_ids: + from models.database.vendor_platform import VendorPlatform + + for platform_id in vendor_data.platform_ids: + # Verify platform exists + platform = db.query(Platform).filter(Platform.id == platform_id).first() + if platform: + vendor_platform = VendorPlatform( + vendor_id=vendor.id, + platform_id=platform_id, + is_active=True, + ) + db.add(vendor_platform) + logger.debug( + f"Assigned vendor {vendor.vendor_code} to platform {platform.code}" + ) + db.flush() db.refresh(vendor) diff --git a/app/templates/admin/user-create.html b/app/templates/admin/user-create.html index 409e41e2..212d693e 100644 --- a/app/templates/admin/user-create.html +++ b/app/templates/admin/user-create.html @@ -81,6 +81,7 @@ - Vendor: Can own companies and manage stores. Admin: Full platform access. + Vendor: Can own companies and manage stores. Admin: Platform management access. + + + diff --git a/app/templates/admin/vendor-create.html b/app/templates/admin/vendor-create.html index 726a081b..8a1d34a3 100644 --- a/app/templates/admin/vendor-create.html +++ b/app/templates/admin/vendor-create.html @@ -166,6 +166,44 @@ + +
+

Platform Access

+
+

+ + Select which platforms this vendor should have access to. Each platform can have different settings and features. +

+
+ +
+ +
+

+ No platforms available. Create a platform first. +

+

+ + Select at least one platform for the vendor to be accessible. +

+
+

Marketplace URLs (Optional)

diff --git a/models/schema/vendor.py b/models/schema/vendor.py index 5be576df..66fc1c86 100644 --- a/models/schema/vendor.py +++ b/models/schema/vendor.py @@ -50,6 +50,11 @@ class VendorCreate(BaseModel): ) description: str | None = Field(None, description="Vendor/brand description") + # Platform assignments (optional - vendor can be on multiple platforms) + platform_ids: list[int] | None = Field( + None, description="List of platform IDs to assign the vendor to" + ) + # Marketplace URLs (brand-specific multi-language support) letzshop_csv_url_fr: str | None = Field(None, description="French CSV URL") letzshop_csv_url_en: str | None = Field(None, description="English CSV URL") diff --git a/static/admin/js/user-create.js b/static/admin/js/user-create.js index c185045f..aa97f917 100644 --- a/static/admin/js/user-create.js +++ b/static/admin/js/user-create.js @@ -17,8 +17,11 @@ function adminUserCreate() { password: '', first_name: '', last_name: '', - role: 'vendor' + role: 'vendor', + is_super_admin: false, + platform_ids: [] }, + platforms: [], errors: {}, saving: false, @@ -33,30 +36,114 @@ function adminUserCreate() { } window._userCreateInitialized = true; + // Load platforms for admin assignment + await this.loadPlatforms(); + userCreateLog.info('=== USER CREATE PAGE INITIALIZATION COMPLETE ==='); }, + // Load available platforms + async loadPlatforms() { + try { + userCreateLog.debug('Loading platforms...'); + const response = await apiClient.get('/admin/platforms'); + this.platforms = response.platforms || response.items || []; + userCreateLog.debug(`Loaded ${this.platforms.length} platforms`); + } catch (error) { + userCreateLog.error('Failed to load platforms:', error); + this.platforms = []; + } + }, + + // Handle role change + onRoleChange() { + userCreateLog.debug('Role changed to:', this.formData.role); + if (this.formData.role !== 'admin') { + // Reset admin-specific fields when switching away from admin + this.formData.is_super_admin = false; + this.formData.platform_ids = []; + } + }, + + // Validate form + validateForm() { + this.errors = {}; + + if (!this.formData.username.trim()) { + this.errors.username = 'Username is required'; + } + if (!this.formData.email.trim()) { + this.errors.email = 'Email is required'; + } + if (!this.formData.password || this.formData.password.length < 6) { + this.errors.password = 'Password must be at least 6 characters'; + } + + // Admin-specific validation + if (this.formData.role === 'admin' && !this.formData.is_super_admin) { + if (!this.formData.platform_ids || this.formData.platform_ids.length === 0) { + this.errors.platform_ids = 'Platform admins must be assigned to at least one platform'; + } + } + + return Object.keys(this.errors).length === 0; + }, + // Submit form async handleSubmit() { userCreateLog.info('=== CREATING USER ==='); userCreateLog.debug('Form data:', { ...this.formData, password: '[REDACTED]' }); - this.errors = {}; + if (!this.validateForm()) { + userCreateLog.warn('Validation failed:', this.errors); + Utils.showToast('Please fix the errors before submitting', 'error'); + return; + } + this.saving = true; try { - const url = `/admin/users`; - window.LogConfig.logApiCall('POST', url, { ...this.formData, password: '[REDACTED]' }, 'request'); + let url, payload, response; + + if (this.formData.role === 'admin') { + // Use admin-users endpoint for creating admin users + url = '/api/v1/admin/admin-users'; + payload = { + email: this.formData.email, + username: this.formData.username, + password: this.formData.password, + first_name: this.formData.first_name || null, + last_name: this.formData.last_name || null, + is_super_admin: this.formData.is_super_admin, + platform_ids: this.formData.is_super_admin ? [] : this.formData.platform_ids.map(id => parseInt(id)) + }; + } else { + // Use regular users endpoint for vendor users + url = '/admin/users'; + payload = { + email: this.formData.email, + username: this.formData.username, + password: this.formData.password, + first_name: this.formData.first_name || null, + last_name: this.formData.last_name || null, + role: this.formData.role + }; + } + + window.LogConfig.logApiCall('POST', url, { ...payload, password: '[REDACTED]' }, 'request'); const startTime = performance.now(); - const response = await apiClient.post(url, this.formData); + response = await apiClient.post(url, payload); const duration = performance.now() - startTime; window.LogConfig.logApiCall('POST', url, response, 'response'); window.LogConfig.logPerformance('Create User', duration); - Utils.showToast('User created successfully', 'success'); - userCreateLog.info(`User created successfully in ${duration}ms`, response); + const userType = this.formData.role === 'admin' + ? (this.formData.is_super_admin ? 'Super admin' : 'Platform admin') + : 'User'; + Utils.showToast(`${userType} created successfully`, 'success'); + userCreateLog.info(`${userType} created successfully in ${duration}ms`, response); // Redirect to the new user's detail page setTimeout(() => { @@ -79,9 +166,9 @@ function adminUserCreate() { // Handle specific errors if (error.message) { - if (error.message.includes('Email already registered')) { + if (error.message.includes('Email already')) { this.errors.email = 'This email is already registered'; - } else if (error.message.includes('Username already taken')) { + } else if (error.message.includes('Username already')) { this.errors.username = 'This username is already taken'; } } diff --git a/static/admin/js/vendor-create.js b/static/admin/js/vendor-create.js index 63c0a685..cd01f27c 100644 --- a/static/admin/js/vendor-create.js +++ b/static/admin/js/vendor-create.js @@ -23,6 +23,9 @@ function adminVendorCreate() { companies: [], loadingCompanies: true, + // Platforms list for selection + platforms: [], + // Form data matching VendorCreate schema formData: { company_id: '', @@ -32,7 +35,8 @@ function adminVendorCreate() { description: '', letzshop_csv_url_fr: '', letzshop_csv_url_en: '', - letzshop_csv_url_de: '' + letzshop_csv_url_de: '', + platform_ids: [] }, // UI state @@ -49,7 +53,10 @@ function adminVendorCreate() { try { vendorCreateLog.info('Initializing vendor create page'); - await this.loadCompanies(); + await Promise.all([ + this.loadCompanies(), + this.loadPlatforms() + ]); } catch (error) { vendorCreateLog.error('Failed to initialize vendor create:', error); } @@ -70,6 +77,18 @@ function adminVendorCreate() { } }, + // Load platforms for selection + async loadPlatforms() { + try { + const response = await apiClient.get('/admin/platforms'); + this.platforms = response.platforms || response.items || []; + vendorCreateLog.debug('Loaded platforms:', this.platforms.length); + } catch (error) { + vendorCreateLog.error('Failed to load platforms:', error); + this.platforms = []; + } + }, + // Auto-generate subdomain from vendor name autoGenerateSubdomain() { if (!this.formData.name) { @@ -124,6 +143,11 @@ function adminVendorCreate() { payload.letzshop_csv_url_de = this.formData.letzshop_csv_url_de; } + // Add platform assignments + if (this.formData.platform_ids && this.formData.platform_ids.length > 0) { + payload.platform_ids = this.formData.platform_ids.map(id => parseInt(id)); + } + const response = await apiClient.post('/admin/vendors', payload); vendorCreateLog.info('Vendor created successfully:', response.vendor_code); @@ -147,7 +171,8 @@ function adminVendorCreate() { description: '', letzshop_csv_url_fr: '', letzshop_csv_url_en: '', - letzshop_csv_url_de: '' + letzshop_csv_url_de: '', + platform_ids: [] }; // Scroll to top to show success message