diff --git a/app/api/v1/admin/__init__.py b/app/api/v1/admin/__init__.py index 2d261c8a..13459252 100644 --- a/app/api/v1/admin/__init__.py +++ b/app/api/v1/admin/__init__.py @@ -28,8 +28,10 @@ from . import ( audit, auth, code_quality, + companies, content_pages, dashboard, + logs, marketplace, monitoring, notifications, @@ -53,9 +55,12 @@ router.include_router(auth.router, tags=["admin-auth"]) # ============================================================================ -# Vendor Management +# Company & Vendor Management # ============================================================================ +# Include company management endpoints +router.include_router(companies.router, tags=["admin-companies"]) + # Include vendor management endpoints router.include_router(vendors.router, tags=["admin-vendors"]) @@ -111,6 +116,9 @@ router.include_router(settings.router, tags=["admin-settings"]) # Include notifications and alerts endpoints router.include_router(notifications.router, tags=["admin-notifications"]) +# Include log management endpoints +router.include_router(logs.router, tags=["admin-logs"]) + # ============================================================================ # Code Quality & Architecture diff --git a/app/routes/admin_pages.py b/app/routes/admin_pages.py index 45169c9c..31368aac 100644 --- a/app/routes/admin_pages.py +++ b/app/routes/admin_pages.py @@ -17,6 +17,7 @@ Routes: - GET /vendors/{vendor_code} → Vendor details (auth required) - GET /vendors/{vendor_code}/edit → Edit vendor form (auth required) - GET /vendors/{vendor_code}/domains → Vendor domains management (auth required) +- GET /vendor-themes → Vendor themes selection page (auth required) - GET /vendors/{vendor_code}/theme → Vendor theme editor (auth required) - GET /users → User management page (auth required) - GET /imports → Import history page (auth required) @@ -109,6 +110,48 @@ async def admin_dashboard_page( ) +# ============================================================================ +# COMPANY MANAGEMENT ROUTES +# ============================================================================ + + +@router.get("/companies", response_class=HTMLResponse, include_in_schema=False) +async def admin_companies_list_page( + request: Request, + current_user: User = Depends(get_current_admin_from_cookie_or_header), + db: Session = Depends(get_db), +): + """ + Render companies management page. + Shows list of all companies with stats. + """ + return templates.TemplateResponse( + "admin/companies.html", + { + "request": request, + "user": current_user, + }, + ) + + +@router.get("/companies/create", response_class=HTMLResponse, include_in_schema=False) +async def admin_company_create_page( + request: Request, + current_user: User = Depends(get_current_admin_from_cookie_or_header), + db: Session = Depends(get_db), +): + """ + Render company creation form. + """ + return templates.TemplateResponse( + "admin/company-create.html", + { + "request": request, + "user": current_user, + }, + ) + + # ============================================================================ # VENDOR MANAGEMENT ROUTES # ============================================================================ @@ -231,6 +274,25 @@ async def admin_vendor_domains_page( # ============================================================================ +@router.get("/vendor-themes", response_class=HTMLResponse, include_in_schema=False) +async def admin_vendor_themes_page( + request: Request, + current_user: User = Depends(get_current_admin_from_cookie_or_header), + db: Session = Depends(get_db), +): + """ + Render vendor themes selection page. + Allows admins to select a vendor to customize their theme. + """ + return templates.TemplateResponse( + "admin/vendor-themes.html", + { + "request": request, + "user": current_user, + }, + ) + + @router.get( "/vendors/{vendor_code}/theme", response_class=HTMLResponse, include_in_schema=False ) @@ -302,6 +364,25 @@ async def admin_imports_page( ) +@router.get("/marketplace", response_class=HTMLResponse, include_in_schema=False) +async def admin_marketplace_page( + request: Request, + current_user: User = Depends(get_current_admin_from_cookie_or_header), + db: Session = Depends(get_db), +): + """ + Render marketplace import management page. + Allows admins to import products for any vendor and monitor all imports. + """ + return templates.TemplateResponse( + "admin/marketplace.html", + { + "request": request, + "user": current_user, + }, + ) + + # ============================================================================ # SETTINGS ROUTES # ============================================================================ @@ -326,6 +407,25 @@ async def admin_settings_page( ) +@router.get("/logs", response_class=HTMLResponse, include_in_schema=False) +async def admin_logs_page( + request: Request, + current_user: User = Depends(get_current_admin_from_cookie_or_header), + db: Session = Depends(get_db), +): + """ + Render admin logs viewer page. + View database and file logs with filtering and search. + """ + return templates.TemplateResponse( + "admin/logs.html", + { + "request": request, + "user": current_user, + }, + ) + + # ============================================================================ # CONTENT MANAGEMENT SYSTEM (CMS) ROUTES # ============================================================================ diff --git a/app/templates/admin/companies.html b/app/templates/admin/companies.html new file mode 100644 index 00000000..9ce0f956 --- /dev/null +++ b/app/templates/admin/companies.html @@ -0,0 +1,272 @@ +{# app/templates/admin/companies.html #} +{% extends "admin/base.html" %} + +{% block title %}Companies{% endblock %} + +{% block alpine_data %}adminCompanies(){% endblock %} + +{% block content %} + +
+

+ Company Management +

+ + + Create Company + +
+ + +
+ +

Loading companies...

+
+ + +
+ +
+

Error loading companies

+

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

+ Total Companies +

+

+ 0 +

+
+
+ + +
+
+ +
+
+

+ Verified +

+

+ 0 +

+
+
+ + +
+
+ +
+
+

+ Active +

+

+ 0 +

+
+
+ + +
+
+ +
+
+

+ Total Vendors +

+

+ 0 +

+
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + +
CompanyOwnerVendorsStatusCreatedActions
+
+ + +
+ + + Showing - of + + + + + + + +
+
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} diff --git a/app/templates/admin/company-create.html b/app/templates/admin/company-create.html new file mode 100644 index 00000000..38c8f3a7 --- /dev/null +++ b/app/templates/admin/company-create.html @@ -0,0 +1,309 @@ +{# app/templates/admin/company-create.html #} +{% extends "admin/base.html" %} + +{% block title %}Create Company{% endblock %} + +{% block alpine_data %}adminCompanyCreate(){% endblock %} + +{% block content %} + +
+
+

+ Create New Company +

+

+ Create a company account with an owner user +

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

Company Created Successfully!

+
+

Owner Login Credentials (Save these!):

+
+
Email:
+
Password:
+
Login URL:
+
+

⚠️ The password will only be shown once. Please save it now!

+
+
+
+
+ + +
+
+ +
+

Error Creating Company

+

+
+
+
+ + +
+
+ +
+

Company Information

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

Public business contact email

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

Owner Account

+
+

+ + A user account will be created for the company owner. If the email already exists, that user will be assigned as owner. +

+
+ +
+ + +

This email will be used for owner login. Can be different from business contact email.

+
+
+ + +
+ + +
+
+
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} diff --git a/app/templates/admin/partials/sidebar.html b/app/templates/admin/partials/sidebar.html index 7c59e80f..07073404 100644 --- a/app/templates/admin/partials/sidebar.html +++ b/app/templates/admin/partials/sidebar.html @@ -20,6 +20,17 @@ @@ -83,6 +94,17 @@ Content Pages + + +
  • + + + + Vendor Themes + +
  • @@ -138,6 +160,37 @@ + +
    +
    +
    +

    + Platform Monitoring +

    + +

    @@ -196,6 +249,17 @@ @@ -259,6 +323,17 @@ Content Pages + + +
  • + + + + Vendor Themes + +
  • @@ -314,6 +389,37 @@ + +
    +
    +
    +

    + Platform Monitoring +

    + +

    diff --git a/static/admin/js/companies.js b/static/admin/js/companies.js new file mode 100644 index 00000000..e2b7523f --- /dev/null +++ b/static/admin/js/companies.js @@ -0,0 +1,237 @@ +// static/admin/js/companies.js + +// ✅ Use centralized logger +const companiesLog = window.LogConfig.loggers.companies || window.LogConfig.createLogger('companies'); + +// ============================================ +// COMPANY LIST FUNCTION +// ============================================ +function adminCompanies() { + return { + // Inherit base layout functionality + ...data(), + + // ✅ Page identifier for sidebar active state + currentPage: 'companies', + + // Companies page specific state + companies: [], + stats: { + total: 0, + verified: 0, + active: 0, + totalVendors: 0 + }, + loading: false, + error: null, + + // Pagination state + page: 1, + itemsPerPage: 10, + + // Initialize + async init() { + companiesLog.info('=== COMPANIES PAGE INITIALIZING ==='); + + // Prevent multiple initializations + if (window._companiesInitialized) { + companiesLog.warn('Companies page already initialized, skipping...'); + return; + } + window._companiesInitialized = true; + + companiesLog.group('Loading companies data'); + await this.loadCompanies(); + await this.loadStats(); + companiesLog.groupEnd(); + + companiesLog.info('=== COMPANIES PAGE INITIALIZATION COMPLETE ==='); + }, + + // Computed: Get paginated companies for current page + get paginatedCompanies() { + const start = (this.page - 1) * this.itemsPerPage; + const end = start + this.itemsPerPage; + return this.companies.slice(start, end); + }, + + // Computed: Total number of pages + get totalPages() { + return Math.ceil(this.companies.length / this.itemsPerPage); + }, + + // Computed: Start index for pagination display + get startIndex() { + if (this.companies.length === 0) return 0; + return (this.page - 1) * this.itemsPerPage + 1; + }, + + // Computed: End index for pagination display + get endIndex() { + const end = this.page * this.itemsPerPage; + return end > this.companies.length ? this.companies.length : end; + }, + + // Computed: Generate page numbers array with ellipsis + get pageNumbers() { + const pages = []; + const totalPages = this.totalPages; + const current = this.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 all companies + async loadCompanies() { + this.loading = true; + this.error = null; + + try { + companiesLog.info('Fetching companies from API...'); + const response = await apiClient.get('/admin/companies'); + + if (response.companies) { + this.companies = response.companies; + companiesLog.info(`Loaded ${this.companies.length} companies`); + } else { + companiesLog.warn('No companies in response'); + this.companies = []; + } + } catch (error) { + companiesLog.error('Failed to load companies:', error); + this.error = error.message || 'Failed to load companies'; + this.companies = []; + } finally { + this.loading = false; + } + }, + + // Load statistics + async loadStats() { + try { + companiesLog.info('Calculating stats from companies...'); + + this.stats.total = this.companies.length; + this.stats.verified = this.companies.filter(c => c.is_verified).length; + this.stats.active = this.companies.filter(c => c.is_active).length; + this.stats.totalVendors = this.companies.reduce((sum, c) => sum + (c.vendor_count || 0), 0); + + companiesLog.info('Stats calculated:', this.stats); + } catch (error) { + companiesLog.error('Failed to calculate stats:', error); + } + }, + + // Edit company + editCompany(companyId) { + companiesLog.info('Edit company:', companyId); + // TODO: Navigate to edit page + window.location.href = `/admin/companies/${companyId}/edit`; + }, + + // Delete company + async deleteCompany(company) { + if (company.vendor_count > 0) { + companiesLog.warn('Cannot delete company with vendors'); + alert(`Cannot delete "${company.name}" because it has ${company.vendor_count} vendor(s). Please delete or reassign the vendors first.`); + return; + } + + const confirmed = confirm( + `Are you sure you want to delete "${company.name}"?\n\nThis action cannot be undone.` + ); + + if (!confirmed) { + companiesLog.info('Delete cancelled by user'); + return; + } + + try { + companiesLog.info('Deleting company:', company.id); + + await apiClient.delete(`/admin/companies/${company.id}?confirm=true`); + + companiesLog.info('Company deleted successfully'); + + // Reload companies + await this.loadCompanies(); + await this.loadStats(); + + alert(`Company "${company.name}" deleted successfully`); + } catch (error) { + companiesLog.error('Failed to delete company:', error); + alert(`Failed to delete company: ${error.message}`); + } + }, + + // Pagination methods + previousPage() { + if (this.page > 1) { + this.page--; + companiesLog.info('Previous page:', this.page); + } + }, + + nextPage() { + if (this.page < this.totalPages) { + this.page++; + companiesLog.info('Next page:', this.page); + } + }, + + goToPage(pageNum) { + if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.totalPages) { + this.page = pageNum; + companiesLog.info('Go to page:', this.page); + } + }, + + // Format date for display + formatDate(dateString) { + if (!dateString) return 'N/A'; + try { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }); + } catch (e) { + companiesLog.error('Date parsing error:', e); + return dateString; + } + } + }; +} + +// Register logger for configuration +if (!window.LogConfig.loggers.companies) { + window.LogConfig.loggers.companies = window.LogConfig.createLogger('companies'); +} + +companiesLog.info('✅ Companies module loaded');