diff --git a/app/routes/admin_pages.py b/app/routes/admin_pages.py index 31368aac..23d446e7 100644 --- a/app/routes/admin_pages.py +++ b/app/routes/admin_pages.py @@ -12,6 +12,9 @@ Routes: - GET / → Redirect to /admin/login - GET /login → Admin login page (no auth) - GET /dashboard → Admin dashboard (auth required) +- GET /companies → Company list page (auth required) +- GET /companies/create → Create company form (auth required) +- GET /companies/{company_id}/edit → Edit company form (auth required) - GET /vendors → Vendor list page (auth required) - GET /vendors/create → Create vendor form (auth required) - GET /vendors/{vendor_code} → Vendor details (auth required) @@ -152,6 +155,50 @@ async def admin_company_create_page( ) +@router.get( + "/companies/{company_id}", response_class=HTMLResponse, include_in_schema=False +) +async def admin_company_detail_page( + request: Request, + company_id: int = Path(..., description="Company ID"), + current_user: User = Depends(get_current_admin_from_cookie_or_header), + db: Session = Depends(get_db), +): + """ + Render company detail view. + """ + return templates.TemplateResponse( + "admin/company-detail.html", + { + "request": request, + "user": current_user, + "company_id": company_id, + }, + ) + + +@router.get( + "/companies/{company_id}/edit", response_class=HTMLResponse, include_in_schema=False +) +async def admin_company_edit_page( + request: Request, + company_id: int = Path(..., description="Company ID"), + current_user: User = Depends(get_current_admin_from_cookie_or_header), + db: Session = Depends(get_db), +): + """ + Render company edit form. + """ + return templates.TemplateResponse( + "admin/company-edit.html", + { + "request": request, + "user": current_user, + "company_id": company_id, + }, + ) + + # ============================================================================ # VENDOR MANAGEMENT ROUTES # ============================================================================ diff --git a/app/templates/admin/companies.html b/app/templates/admin/companies.html index 9ce0f956..8b441213 100644 --- a/app/templates/admin/companies.html +++ b/app/templates/admin/companies.html @@ -178,6 +178,15 @@
+ + + + + +
+ + + +
+ +
+
+ +
+
+

+ Verification +

+

+ - +

+
+
+ + +
+
+ +
+
+

+ Status +

+

+ - +

+
+
+ + +
+
+ +
+
+

+ Vendors +

+

+ 0 +

+
+
+ + +
+
+ +
+
+

+ Created +

+

+ - +

+
+
+
+ + +
+ +
+

+ Basic Information +

+
+
+

Company Name

+

-

+
+
+

Description

+

-

+
+
+
+ + +
+

+ Contact Information +

+
+
+

Contact Email

+

-

+
+
+

Phone

+

-

+
+
+

Website

+ + + - +
+
+
+
+ + +
+

+ Business Details +

+
+
+

Business Address

+

-

+
+
+

Tax Number

+

-

+
+
+
+ + +
+

+ Owner Information +

+
+
+

Owner User ID

+

-

+
+
+

Owner Username

+

-

+
+
+

Owner Email

+

-

+
+
+
+ + +
+

+ + Vendors () +

+
+ + + + + + + + + + + + +
VendorSubdomainStatusActions
+
+
+ + +
+

+ More Actions +

+
+ + + + Create Vendor + +
+

+ + Vendors created will be associated with this company. +

+
+ +{% endblock %} + +{% block extra_scripts %} + +{% endblock %} diff --git a/app/templates/admin/company-edit.html b/app/templates/admin/company-edit.html new file mode 100644 index 00000000..5ebd1d6e --- /dev/null +++ b/app/templates/admin/company-edit.html @@ -0,0 +1,496 @@ +{# app/templates/admin/company-edit.html #} +{% extends "admin/base.html" %} + +{% block title %}Edit Company{% endblock %} + +{% block alpine_data %}adminCompanyEdit(){% endblock %} + +{% block content %} + +
+
+

+ Edit Company +

+

+ +

+
+ + + Back to Companies + +
+ + +
+ +

Loading company...

+
+ + +
+ +
+

+ Quick Actions +

+
+ + + + + +
+ + + Verified + + + + Pending + + + Active + + + Inactive + +
+
+
+ + +
+
+ +
+

+ Basic Information +

+ + + + + + + + + +
+ + +
+

+ Contact Information +

+ + + + + + + + + + + + +
+
+ + +
+

+ Business Details +

+ +
+ + + + + +
+
+ + + + + +
+ + Cancel + + +
+
+ + +
+

+ More Actions +

+
+ + + + + +
+

+ + Ownership transfer affects all vendors under this company. + + Company cannot be deleted while it has vendors ( vendors). + +

+
+ + +
+
+ +
+

+ Transfer Company Ownership +

+ +
+ + +
+
+

+ + + Warning: This will transfer ownership of the company + "" and all its vendors to another user. + +

+
+ +
+ + + + + + + + +

+ + Please confirm the transfer by checking the box above +

+
+ + +
+ + +
+
+
+
+
+
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} diff --git a/static/admin/js/company-detail.js b/static/admin/js/company-detail.js new file mode 100644 index 00000000..a894255d --- /dev/null +++ b/static/admin/js/company-detail.js @@ -0,0 +1,145 @@ +// static/admin/js/company-detail.js + +// Create custom logger for company detail +const companyDetailLog = window.LogConfig.createLogger('COMPANY-DETAIL'); + +function adminCompanyDetail() { + return { + // Inherit base layout functionality from init-alpine.js + ...data(), + + // Company detail page specific state + currentPage: 'company-detail', + company: null, + loading: false, + error: null, + companyId: null, + + // Initialize + async init() { + companyDetailLog.info('=== COMPANY DETAIL PAGE INITIALIZING ==='); + + // Prevent multiple initializations + if (window._companyDetailInitialized) { + companyDetailLog.warn('Company detail page already initialized, skipping...'); + return; + } + window._companyDetailInitialized = true; + + // Get company ID from URL + const path = window.location.pathname; + const match = path.match(/\/admin\/companies\/(\d+)$/); + + if (match) { + this.companyId = match[1]; + companyDetailLog.info('Viewing company:', this.companyId); + await this.loadCompany(); + } else { + companyDetailLog.error('No company ID in URL'); + this.error = 'Invalid company URL'; + Utils.showToast('Invalid company URL', 'error'); + } + + companyDetailLog.info('=== COMPANY DETAIL PAGE INITIALIZATION COMPLETE ==='); + }, + + // Load company data + async loadCompany() { + companyDetailLog.info('Loading company details...'); + this.loading = true; + this.error = null; + + try { + const url = `/admin/companies/${this.companyId}`; + 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 Company Details', duration); + + this.company = response; + + companyDetailLog.info(`Company loaded in ${duration}ms`, { + id: this.company.id, + name: this.company.name, + is_verified: this.company.is_verified, + is_active: this.company.is_active, + vendor_count: this.company.vendor_count + }); + companyDetailLog.debug('Full company data:', this.company); + + } catch (error) { + window.LogConfig.logError(error, 'Load Company Details'); + this.error = error.message || 'Failed to load company details'; + Utils.showToast('Failed to load company details', 'error'); + } finally { + this.loading = false; + } + }, + + // Format date (matches dashboard pattern) + formatDate(dateString) { + if (!dateString) { + companyDetailLog.debug('formatDate called with empty dateString'); + return '-'; + } + const formatted = Utils.formatDate(dateString); + companyDetailLog.debug(`Date formatted: ${dateString} -> ${formatted}`); + return formatted; + }, + + // Delete company + async deleteCompany() { + companyDetailLog.info('Delete company requested:', this.companyId); + + if (this.company?.vendor_count > 0) { + Utils.showToast(`Cannot delete company with ${this.company.vendor_count} vendor(s). Delete vendors first.`, 'error'); + return; + } + + if (!confirm(`Are you sure you want to delete company "${this.company.name}"?\n\nThis action cannot be undone.`)) { + companyDetailLog.info('Delete cancelled by user'); + return; + } + + // Second confirmation for safety + if (!confirm(`FINAL CONFIRMATION\n\nAre you absolutely sure you want to delete "${this.company.name}"?`)) { + companyDetailLog.info('Delete cancelled by user (second confirmation)'); + return; + } + + try { + const url = `/admin/companies/${this.companyId}?confirm=true`; + window.LogConfig.logApiCall('DELETE', url, null, 'request'); + + companyDetailLog.info('Deleting company:', this.companyId); + await apiClient.delete(url); + + window.LogConfig.logApiCall('DELETE', url, null, 'response'); + + Utils.showToast('Company deleted successfully', 'success'); + companyDetailLog.info('Company deleted successfully'); + + // Redirect to companies list + setTimeout(() => window.location.href = '/admin/companies', 1500); + + } catch (error) { + window.LogConfig.logError(error, 'Delete Company'); + Utils.showToast(error.message || 'Failed to delete company', 'error'); + } + }, + + // Refresh company data + async refresh() { + companyDetailLog.info('=== COMPANY REFRESH TRIGGERED ==='); + await this.loadCompany(); + Utils.showToast('Company details refreshed', 'success'); + companyDetailLog.info('=== COMPANY REFRESH COMPLETE ==='); + } + }; +} + +companyDetailLog.info('Company detail module loaded'); diff --git a/static/admin/js/company-edit.js b/static/admin/js/company-edit.js new file mode 100644 index 00000000..424af655 --- /dev/null +++ b/static/admin/js/company-edit.js @@ -0,0 +1,386 @@ +// static/admin/js/company-edit.js + +// Create custom logger for company edit +const companyEditLog = window.LogConfig.createLogger('COMPANY-EDIT'); + +function adminCompanyEdit() { + return { + // Inherit base layout functionality from init-alpine.js + ...data(), + + // Company edit page specific state + currentPage: 'company-edit', + company: null, + formData: {}, + errors: {}, + loadingCompany: false, + saving: false, + companyId: null, + + // Transfer ownership state + showTransferOwnershipModal: false, + transferring: false, + transferData: { + new_owner_user_id: null, + confirm_transfer: false, + transfer_reason: '' + }, + + // User search state + userSearchQuery: '', + userSearchResults: [], + selectedUser: null, + showUserDropdown: false, + searchingUsers: false, + searchDebounceTimer: null, + showConfirmError: false, + showOwnerError: false, + + // Initialize + async init() { + companyEditLog.info('=== COMPANY EDIT PAGE INITIALIZING ==='); + + // Prevent multiple initializations + if (window._companyEditInitialized) { + companyEditLog.warn('Company edit page already initialized, skipping...'); + return; + } + window._companyEditInitialized = true; + + // Get company ID from URL + const path = window.location.pathname; + const match = path.match(/\/admin\/companies\/(\d+)\/edit/); + + if (match) { + this.companyId = parseInt(match[1], 10); + companyEditLog.info('Editing company:', this.companyId); + await this.loadCompany(); + } else { + companyEditLog.error('No company ID in URL'); + Utils.showToast('Invalid company URL', 'error'); + setTimeout(() => window.location.href = '/admin/companies', 2000); + } + + companyEditLog.info('=== COMPANY EDIT PAGE INITIALIZATION COMPLETE ==='); + }, + + // Load company data + async loadCompany() { + companyEditLog.info('Loading company data...'); + this.loadingCompany = true; + + try { + const url = `/admin/companies/${this.companyId}`; + 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 Company', duration); + + this.company = response; + + // Initialize form data + this.formData = { + name: response.name || '', + description: response.description || '', + contact_email: response.contact_email || '', + contact_phone: response.contact_phone || '', + website: response.website || '', + business_address: response.business_address || '', + tax_number: response.tax_number || '' + }; + + companyEditLog.info(`Company loaded in ${duration}ms`, { + company_id: this.company.id, + name: this.company.name + }); + companyEditLog.debug('Form data initialized:', this.formData); + + } catch (error) { + window.LogConfig.logError(error, 'Load Company'); + Utils.showToast('Failed to load company', 'error'); + setTimeout(() => window.location.href = '/admin/companies', 2000); + } finally { + this.loadingCompany = false; + } + }, + + // Submit form + async handleSubmit() { + companyEditLog.info('=== SUBMITTING COMPANY UPDATE ==='); + companyEditLog.debug('Form data:', this.formData); + + this.errors = {}; + this.saving = true; + + try { + const url = `/admin/companies/${this.companyId}`; + window.LogConfig.logApiCall('PUT', url, this.formData, 'request'); + + const startTime = performance.now(); + const response = await apiClient.put(url, this.formData); + const duration = performance.now() - startTime; + + window.LogConfig.logApiCall('PUT', url, response, 'response'); + window.LogConfig.logPerformance('Update Company', duration); + + this.company = response; + Utils.showToast('Company updated successfully', 'success'); + companyEditLog.info(`Company updated successfully in ${duration}ms`, response); + + } catch (error) { + window.LogConfig.logError(error, 'Update Company'); + + // Handle validation errors + if (error.details && error.details.validation_errors) { + error.details.validation_errors.forEach(err => { + const field = err.loc?.[1] || err.loc?.[0]; + if (field) { + this.errors[field] = err.msg; + } + }); + companyEditLog.debug('Validation errors:', this.errors); + } + + Utils.showToast(error.message || 'Failed to update company', 'error'); + } finally { + this.saving = false; + companyEditLog.info('=== COMPANY UPDATE COMPLETE ==='); + } + }, + + // Toggle verification + async toggleVerification() { + const action = this.company.is_verified ? 'unverify' : 'verify'; + companyEditLog.info(`Toggle verification: ${action}`); + + if (!confirm(`Are you sure you want to ${action} this company?`)) { + companyEditLog.info('Verification toggle cancelled by user'); + return; + } + + this.saving = true; + try { + const url = `/admin/companies/${this.companyId}/verification`; + const payload = { is_verified: !this.company.is_verified }; + + window.LogConfig.logApiCall('PUT', url, payload, 'request'); + + const response = await apiClient.put(url, payload); + + window.LogConfig.logApiCall('PUT', url, response, 'response'); + + this.company = response; + Utils.showToast(`Company ${action}ed successfully`, 'success'); + companyEditLog.info(`Company ${action}ed successfully`); + + } catch (error) { + window.LogConfig.logError(error, `Toggle Verification (${action})`); + Utils.showToast(`Failed to ${action} company`, 'error'); + } finally { + this.saving = false; + } + }, + + // Toggle active status + async toggleActive() { + const action = this.company.is_active ? 'deactivate' : 'activate'; + companyEditLog.info(`Toggle active status: ${action}`); + + if (!confirm(`Are you sure you want to ${action} this company?\n\nThis will affect all vendors under this company.`)) { + companyEditLog.info('Active status toggle cancelled by user'); + return; + } + + this.saving = true; + try { + const url = `/admin/companies/${this.companyId}/status`; + const payload = { is_active: !this.company.is_active }; + + window.LogConfig.logApiCall('PUT', url, payload, 'request'); + + const response = await apiClient.put(url, payload); + + window.LogConfig.logApiCall('PUT', url, response, 'response'); + + this.company = response; + Utils.showToast(`Company ${action}d successfully`, 'success'); + companyEditLog.info(`Company ${action}d successfully`); + + } catch (error) { + window.LogConfig.logError(error, `Toggle Active Status (${action})`); + Utils.showToast(`Failed to ${action} company`, 'error'); + } finally { + this.saving = false; + } + }, + + // Transfer company ownership + async transferOwnership() { + companyEditLog.info('=== TRANSFERRING COMPANY OWNERSHIP ==='); + companyEditLog.debug('Transfer data:', this.transferData); + + if (!this.transferData.new_owner_user_id) { + this.showOwnerError = true; + return; + } + + if (!this.transferData.confirm_transfer) { + this.showConfirmError = true; + return; + } + + // Clear errors + this.showOwnerError = false; + this.showConfirmError = false; + + this.transferring = true; + try { + const url = `/admin/companies/${this.companyId}/transfer-ownership`; + const payload = { + new_owner_user_id: parseInt(this.transferData.new_owner_user_id, 10), + confirm_transfer: true, + transfer_reason: this.transferData.transfer_reason || null + }; + + window.LogConfig.logApiCall('POST', url, payload, 'request'); + + const response = await apiClient.post(url, payload); + + window.LogConfig.logApiCall('POST', url, response, 'response'); + + Utils.showToast('Ownership transferred successfully', 'success'); + companyEditLog.info('Ownership transferred successfully', response); + + // Close modal and reload company data + this.showTransferOwnershipModal = false; + this.resetTransferData(); + await this.loadCompany(); + + } catch (error) { + window.LogConfig.logError(error, 'Transfer Ownership'); + Utils.showToast(error.message || 'Failed to transfer ownership', 'error'); + } finally { + this.transferring = false; + companyEditLog.info('=== OWNERSHIP TRANSFER COMPLETE ==='); + } + }, + + // Reset transfer data + resetTransferData() { + this.transferData = { + new_owner_user_id: null, + confirm_transfer: false, + transfer_reason: '' + }; + this.userSearchQuery = ''; + this.userSearchResults = []; + this.selectedUser = null; + this.showUserDropdown = false; + this.showConfirmError = false; + this.showOwnerError = false; + }, + + // Search users for transfer ownership + searchUsers() { + // Debounce search + clearTimeout(this.searchDebounceTimer); + + if (this.userSearchQuery.length < 2) { + this.userSearchResults = []; + this.showUserDropdown = false; + return; + } + + this.searchDebounceTimer = setTimeout(async () => { + companyEditLog.info('Searching users:', this.userSearchQuery); + this.searchingUsers = true; + this.showUserDropdown = true; + + try { + const url = `/admin/users/search?q=${encodeURIComponent(this.userSearchQuery)}&limit=10`; + const response = await apiClient.get(url); + + this.userSearchResults = response.users || response || []; + companyEditLog.debug('User search results:', this.userSearchResults); + + } catch (error) { + window.LogConfig.logError(error, 'Search Users'); + this.userSearchResults = []; + } finally { + this.searchingUsers = false; + } + }, 300); + }, + + // Select a user from search results + selectUser(user) { + companyEditLog.info('Selected user:', user); + this.selectedUser = user; + this.transferData.new_owner_user_id = user.id; + this.userSearchQuery = user.username; + this.showUserDropdown = false; + this.userSearchResults = []; + }, + + // Clear selected user + clearSelectedUser() { + this.selectedUser = null; + this.transferData.new_owner_user_id = null; + this.userSearchQuery = ''; + this.userSearchResults = []; + }, + + // Delete company + async deleteCompany() { + companyEditLog.info('=== DELETING COMPANY ==='); + + if (this.company.vendor_count > 0) { + Utils.showToast(`Cannot delete company with ${this.company.vendor_count} vendors. Remove vendors first.`, 'error'); + return; + } + + if (!confirm(`Are you sure you want to delete company "${this.company.name}"?\n\nThis action cannot be undone.`)) { + companyEditLog.info('Company deletion cancelled by user'); + return; + } + + // Double confirmation for critical action + if (!confirm(`FINAL CONFIRMATION: Delete "${this.company.name}"?\n\nThis will permanently delete the company and all its data.`)) { + companyEditLog.info('Company deletion cancelled at final confirmation'); + return; + } + + this.saving = true; + try { + const url = `/admin/companies/${this.companyId}?confirm=true`; + + window.LogConfig.logApiCall('DELETE', url, null, 'request'); + + const response = await apiClient.delete(url); + + window.LogConfig.logApiCall('DELETE', url, response, 'response'); + + Utils.showToast('Company deleted successfully', 'success'); + companyEditLog.info('Company deleted successfully'); + + // Redirect to companies list + setTimeout(() => { + window.location.href = '/admin/companies'; + }, 1500); + + } catch (error) { + window.LogConfig.logError(error, 'Delete Company'); + Utils.showToast(error.message || 'Failed to delete company', 'error'); + } finally { + this.saving = false; + companyEditLog.info('=== COMPANY DELETION COMPLETE ==='); + } + } + }; +} + +companyEditLog.info('Company edit module loaded');