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 @@
+
+
+
+
+
@@ -259,7 +261,7 @@ function adminCompanyCreate() {
try {
console.log('Creating company:', this.formData);
- const response = await apiClient.post('/api/v1/admin/companies', this.formData);
+ const response = await apiClient.post('/admin/companies', this.formData);
console.log('Company created successfully:', response);
diff --git a/app/templates/admin/company-detail.html b/app/templates/admin/company-detail.html
new file mode 100644
index 00000000..d6d97c36
--- /dev/null
+++ b/app/templates/admin/company-detail.html
@@ -0,0 +1,291 @@
+{# app/templates/admin/company-detail.html #}
+{% extends "admin/base.html" %}
+
+{% block title %}Company Details{% endblock %}
+
+{% block alpine_data %}adminCompanyDetail(){% endblock %}
+
+{% block content %}
+
+
+
+
+ Company Details
+
+
+ ID:
+ |
+ vendor(s)
+
+
+
+
+ Back
+
+
+
+
+
+
+ Loading company details...
+
+
+
+
+
+
+ Error loading company
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Verification
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Basic Information
+
+
+
+
+
+
+
+ Contact Information
+
+
+
+
+
+
+
+
+ Business Details
+
+
+
+
+
+
+
+ Owner Information
+
+
+
+
+
+
+
+
+ Vendors ()
+
+
+
+
+
+ | Vendor |
+ Subdomain |
+ Status |
+ Actions |
+
+
+
+
+
+ |
+
+ |
+ |
+
+
+
+ |
+
+
+ View
+
+ |
+
+
+
+
+
+
+
+
+
+
+ More Actions
+
+
+
+
+ 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 %}
+
+
+
+
+
+
+
+
+
+
+
+ Quick Actions
+
+
+
+
+
+
+
+
+
+
+ Verified
+
+
+
+ Pending
+
+
+ Active
+
+
+ Inactive
+
+
+
+
+
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+
+
+
+
+{% 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');
|