diff --git a/app/api/deps.py b/app/api/deps.py index d9537bd3..327be628 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -113,6 +113,44 @@ def get_current_admin_user(current_user: User = Depends(get_current_user)): return auth_manager.require_admin(current_user) +def get_current_vendor_user(current_user: User = Depends(get_current_user)): + """ + Require vendor user (vendor owner or vendor staff). + + This dependency ensures the current user has vendor role. + Used for protecting vendor-only routes. + + Args: + current_user: User object from get_current_user dependency + + Returns: + User: Vendor user object + + Raises: + InsufficientPermissionsException: If user is not a vendor user + """ + return auth_manager.require_vendor(current_user) + + +def get_current_customer_user(current_user: User = Depends(get_current_user)): + """ + Require customer user. + + This dependency ensures the current user has customer role. + Used for protecting customer account routes. + + Args: + current_user: User object from get_current_user dependency + + Returns: + User: Customer user object + + Raises: + InsufficientPermissionsException: If user is not a customer + """ + return auth_manager.require_customer(current_user) + + def get_user_vendor( vendor_code: str, current_user: User = Depends(get_current_user), diff --git a/app/api/v1/admin/pages.py b/app/api/v1/admin/pages.py index 8740bec8..bafdafb0 100644 --- a/app/api/v1/admin/pages.py +++ b/app/api/v1/admin/pages.py @@ -3,17 +3,25 @@ Admin HTML page routes using Jinja2 templates. These routes return rendered HTML pages (response_class=HTMLResponse). -Separate from other admin routes which return JSON data. +Separate from admin API routes which return JSON data. + +All routes require admin authentication except /login. +Authentication failures redirect to /admin/login. Routes: -- GET / - Admin root (redirects to login) -- GET /login - Admin login page (no auth required) -- GET /dashboard - Admin dashboard (requires auth) -- GET /vendors - Vendor management page (requires auth) -- GET /users - User management page (requires auth) +- GET / → Redirect to /admin/login +- GET /login → Admin login page (no auth) +- GET /dashboard → Admin dashboard (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) +- GET /vendors/{vendor_code}/edit → Edit vendor form (auth required) +- GET /users → User management page (auth required) +- GET /imports → Import history page (auth required) +- GET /settings → Settings page (auth required) """ -from fastapi import APIRouter, Request, Depends +from fastapi import APIRouter, Request, Depends, Path from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates from sqlalchemy.orm import Session @@ -25,18 +33,18 @@ router = APIRouter() templates = Jinja2Templates(directory="app/templates") +# ============================================================================ +# PUBLIC ROUTES (No Authentication Required) +# ============================================================================ + @router.get("/", response_class=RedirectResponse, include_in_schema=False) async def admin_root(): """ Redirect /admin/ to /admin/login. - - This is the simplest approach: - - Unauthenticated users: see login form - - Authenticated users: login page clears token and shows form - (they can manually navigate to dashboard if needed) - - Alternative: Could redirect to /admin/dashboard and let auth - dependency handle the redirect, but that's an extra hop. + + Simple approach: + - Unauthenticated users → see login form + - Authenticated users → login page shows form (they can navigate to dashboard) """ return RedirectResponse(url="/admin/login", status_code=302) @@ -53,15 +61,19 @@ async def admin_login_page(request: Request): ) +# ============================================================================ +# AUTHENTICATED ROUTES (Admin Only) +# ============================================================================ + @router.get("/dashboard", response_class=HTMLResponse, include_in_schema=False) async def admin_dashboard_page( - request: Request, - current_user: User = Depends(get_current_admin_user), - db: Session = Depends(get_db) + request: Request, + current_user: User = Depends(get_current_admin_user), + db: Session = Depends(get_db) ): """ Render admin dashboard page. - Requires admin authentication - will redirect to login if not authenticated. + Shows platform statistics and recent activity. """ return templates.TemplateResponse( "admin/dashboard.html", @@ -72,15 +84,19 @@ async def admin_dashboard_page( ) +# ============================================================================ +# VENDOR MANAGEMENT ROUTES +# ============================================================================ + @router.get("/vendors", response_class=HTMLResponse, include_in_schema=False) -async def admin_vendors_page( - request: Request, - current_user: User = Depends(get_current_admin_user), - db: Session = Depends(get_db) +async def admin_vendors_list_page( + request: Request, + current_user: User = Depends(get_current_admin_user), + db: Session = Depends(get_db) ): """ Render vendors management page. - Requires admin authentication. + Shows list of all vendors with stats. """ return templates.TemplateResponse( "admin/vendors.html", @@ -91,15 +107,78 @@ async def admin_vendors_page( ) +@router.get("/vendors/create", response_class=HTMLResponse, include_in_schema=False) +async def admin_vendor_create_page( + request: Request, + current_user: User = Depends(get_current_admin_user), + db: Session = Depends(get_db) +): + """ + Render vendor creation form. + """ + return templates.TemplateResponse( + "admin/vendor-create.html", + { + "request": request, + "user": current_user, + } + ) + + +@router.get("/vendors/{vendor_code}", response_class=HTMLResponse, include_in_schema=False) +async def admin_vendor_detail_page( + request: Request, + vendor_code: str = Path(..., description="Vendor code"), + current_user: User = Depends(get_current_admin_user), + db: Session = Depends(get_db) +): + """ + Render vendor detail page. + Shows full vendor information. + """ + return templates.TemplateResponse( + "admin/vendor-detail.html", + { + "request": request, + "user": current_user, + "vendor_code": vendor_code, + } + ) + + +@router.get("/vendors/{vendor_code}/edit", response_class=HTMLResponse, include_in_schema=False) +async def admin_vendor_edit_page( + request: Request, + vendor_code: str = Path(..., description="Vendor code"), + current_user: User = Depends(get_current_admin_user), + db: Session = Depends(get_db) +): + """ + Render vendor edit form. + """ + return templates.TemplateResponse( + "admin/vendor-edit.html", + { + "request": request, + "user": current_user, + "vendor_code": vendor_code, + } + ) + + +# ============================================================================ +# USER MANAGEMENT ROUTES +# ============================================================================ + @router.get("/users", response_class=HTMLResponse, include_in_schema=False) async def admin_users_page( - request: Request, - current_user: User = Depends(get_current_admin_user), - db: Session = Depends(get_db) + request: Request, + current_user: User = Depends(get_current_admin_user), + db: Session = Depends(get_db) ): """ Render users management page. - Requires admin authentication. + Shows list of all platform users. """ return templates.TemplateResponse( "admin/users.html", @@ -108,3 +187,49 @@ async def admin_users_page( "user": current_user, } ) + + +# ============================================================================ +# IMPORT MANAGEMENT ROUTES +# ============================================================================ + +@router.get("/imports", response_class=HTMLResponse, include_in_schema=False) +async def admin_imports_page( + request: Request, + current_user: User = Depends(get_current_admin_user), + db: Session = Depends(get_db) +): + """ + Render imports management page. + Shows import history and status. + """ + return templates.TemplateResponse( + "admin/imports.html", + { + "request": request, + "user": current_user, + } + ) + + +# ============================================================================ +# SETTINGS ROUTES +# ============================================================================ + +@router.get("/settings", response_class=HTMLResponse, include_in_schema=False) +async def admin_settings_page( + request: Request, + current_user: User = Depends(get_current_admin_user), + db: Session = Depends(get_db) +): + """ + Render admin settings page. + Platform configuration and preferences. + """ + return templates.TemplateResponse( + "admin/settings.html", + { + "request": request, + "user": current_user, + } + ) diff --git a/app/templates/admin/base.html b/app/templates/admin/base.html index bee5be62..e16f8678 100644 --- a/app/templates/admin/base.html +++ b/app/templates/admin/base.html @@ -37,24 +37,27 @@ - + - + + + + - + - - - - + - + + + + - + {% block extra_scripts %}{% endblock %} \ No newline at end of file diff --git a/app/templates/admin/login.html b/app/templates/admin/login.html index 75ac490a..f5ca948b 100644 --- a/app/templates/admin/login.html +++ b/app/templates/admin/login.html @@ -100,10 +100,24 @@ - + + + + + + + + + + + - + + + + + \ No newline at end of file diff --git a/app/templates/admin/users.html b/app/templates/admin/users.html new file mode 100644 index 00000000..8c7309d4 --- /dev/null +++ b/app/templates/admin/users.html @@ -0,0 +1,270 @@ +{% extends "admin/base.html" %} + +{% block title %}Users Management - LetzShop Admin{% endblock %} + +{% block page_title %}Users Management{% endblock %} + +{% block content %} +
+ +
+
+ +
+
+ +
+
+
+ + +
+ + + + + + + + +
+
+
+ + +
+
+
+
+

Total Users

+

+
+
+
+
+ +
+
+
+

Active Users

+

+
+
+
+
+ +
+
+
+

Vendors

+

+
+
+
+
+ +
+
+
+

Admins

+

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

Loading users...

+
+ + +
+
+

No users found

+
+ + +
+ + + + + + + + + + + + + + + +
+ User + + Email + + Role + + Status + + Registered + + Last Login + + Actions +
+ + +
+
+
+ Showing + to + of users +
+
+ + +
+
+
+
+
+
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} \ No newline at end of file diff --git a/app/templates/admin/vendor-edit.html b/app/templates/admin/vendor-edit.html new file mode 100644 index 00000000..a23b7158 --- /dev/null +++ b/app/templates/admin/vendor-edit.html @@ -0,0 +1,257 @@ +{# app/templates/admin/vendor-edit.html #} +{% extends "admin/base.html" %} + +{% block title %}Edit Vendor{% endblock %} + +{% block alpine_data %}adminVendorEdit(){% endblock %} + +{% block content %} + +
+
+

+ Edit Vendor +

+

+ + + +

+
+ + + Back to Vendors + +
+ + +
+ +

Loading vendor...

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

+ Basic Information +

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

+ Contact Information +

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

+ Business Details +

+ +
+ + + + + +
+
+ + +
+ + Cancel + + +
+
+
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} \ No newline at end of file diff --git a/app/templates/admin/vendors.html b/app/templates/admin/vendors.html new file mode 100644 index 00000000..21c95568 --- /dev/null +++ b/app/templates/admin/vendors.html @@ -0,0 +1,263 @@ +{# app/templates/admin/vendors.html #} +{% extends "admin/base.html" %} + +{% block title %}Vendors{% endblock %} + +{% block alpine_data %}adminVendors(){% endblock %} + +{% block content %} + +
+

+ Vendor Management +

+ + + Create Vendor + +
+ + +
+ +

Loading vendors...

+
+ + +
+ +
+

Error loading vendors

+

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

+ Total Vendors +

+

+ 0 +

+
+
+ + +
+
+ +
+
+

+ Verified Vendors +

+

+ 0 +

+
+
+ + +
+
+ +
+
+

+ Pending +

+

+ 0 +

+
+
+ + +
+
+ +
+
+

+ Inactive +

+

+ 0 +

+
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + +
VendorSubdomainStatusCreatedActions
+
+ + +
+ + + Showing - of + + + + + + + +
+
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} \ No newline at end of file diff --git a/app/templates/partials/header.html b/app/templates/partials/header.html index a0cb30ed..3d0d7473 100644 --- a/app/templates/partials/header.html +++ b/app/templates/partials/header.html @@ -1,3 +1,4 @@ +
@@ -33,82 +34,128 @@ -
  • +
  • - +
  • - -
  • + +
  • - + +
  • -
    \ No newline at end of file + + + + diff --git a/main.py b/main.py index 872f1404..2709a47e 100644 --- a/main.py +++ b/main.py @@ -12,8 +12,11 @@ from sqlalchemy import text from sqlalchemy.orm import Session from app.api.main import api_router -from app.routes.frontend import router as frontend_router # We'll phase this out + +# Import page routers from app.api.v1.admin import pages as admin_pages +from app.api.v1.vendor import pages as vendor_pages +from app.api.v1.public.vendors import pages as shop_pages from app.core.config import settings from app.core.database import get_db from app.core.lifespan import lifespan @@ -67,18 +70,39 @@ else: app.include_router(api_router, prefix="/api") # ============================================================================ -# Include HTML page routes (Jinja2 templates at /admin/*) + +# OLD: Keep frontend router for now (we'll phase it out) +# app.include_router(frontend_router) + # ============================================================================ +# HTML PAGE ROUTES (Jinja2 Templates) +# ============================================================================ + +# Admin pages app.include_router( admin_pages.router, prefix="/admin", tags=["admin-pages"], include_in_schema=False # Don't show HTML pages in API docs ) -# ============================================================================ -# OLD: Keep frontend router for now (we'll phase it out) -app.include_router(frontend_router) +# Vendor pages +app.include_router( + vendor_pages.router, + tags=["vendor-pages"], + include_in_schema=False # Don't show HTML pages in API docs +) + +# Shop pages +app.include_router( + shop_pages.router, + tags=["shop-pages"], + include_in_schema=False # Don't show HTML pages in API docs +) + +# ============================================================================ +# API ROUTES (JSON Responses) +# ============================================================================ # Public Routes (no authentication required) @app.get("/", include_in_schema=False) diff --git a/middleware/auth.py b/middleware/auth.py index 5a51e841..ff63f0c2 100644 --- a/middleware/auth.py +++ b/middleware/auth.py @@ -159,6 +159,52 @@ class AuthManager: raise AdminRequiredException() return current_user + def require_vendor(self, current_user: User): + """ + Require vendor role (vendor or admin). + + Vendors and admins can access vendor areas. + + Args: + current_user: Current authenticated user + + Returns: + User: The user if they have vendor or admin role + + Raises: + InsufficientPermissionsException: If user is not vendor or admin + """ + if current_user.role not in ["vendor", "admin"]: + from app.exceptions import InsufficientPermissionsException + raise InsufficientPermissionsException( + message="Vendor access required", + required_permission="vendor" + ) + return current_user + + def require_customer(self, current_user: User): + """ + Require customer role (customer or admin). + + Customers and admins can access customer account areas. + + Args: + current_user: Current authenticated user + + Returns: + User: The user if they have customer or admin role + + Raises: + InsufficientPermissionsException: If user is not customer or admin + """ + if current_user.role not in ["customer", "admin"]: + from app.exceptions import InsufficientPermissionsException + raise InsufficientPermissionsException( + message="Customer account access required", + required_permission="customer" + ) + return current_user + def create_default_admin_user(self, db: Session): """Create default admin user if it doesn't exist.""" admin_user = db.query(User).filter(User.username == "admin").first() diff --git a/models/database/user.py b/models/database/user.py index 0925ab74..6b0b8699 100644 --- a/models/database/user.py +++ b/models/database/user.py @@ -17,7 +17,7 @@ class User(Base, TimestampMixin): first_name = Column(String) last_name = Column(String) hashed_password = Column(String, nullable=False) - role = Column(String, nullable=False, default="user") # user, admin, vendor_owner + role = Column(String, nullable=False, default="user") # user, admin, vendor_owner TODO: Change to customer, vendor, admin? is_active = Column(Boolean, default=True, nullable=False) last_login = Column(DateTime, nullable=True) diff --git a/static/admin/dashboard.html b/static/admin/dashboard.html deleted file mode 100644 index 4e6a6c96..00000000 --- a/static/admin/dashboard.html +++ /dev/null @@ -1,219 +0,0 @@ - - - - - - Admin Dashboard - Multi-Tenant Platform - - - - - -
    - - - -
    - -
    - - -
    -
    - -
    -

    - Dashboard -

    - -
    - - -
    - -

    Loading dashboard...

    -
    - - -
    - -
    -

    Error loading dashboard

    -

    -
    -
    - - -
    - -
    -
    - -
    -
    -

    - Total Vendors -

    -

    - 0 -

    -
    -
    - - -
    -
    - -
    -
    -

    - Active Users -

    -

    - 0 -

    -
    -
    - - -
    -
    - -
    -
    -

    - Verified Vendors -

    -

    - 0 -

    -
    -
    - - -
    -
    - -
    -
    -

    - Import Jobs -

    -

    - 0 -

    -
    -
    -
    - - -
    -
    - - - - - - - - - - - - - - -
    VendorStatusCreatedActions
    -
    -
    -
    -
    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/static/admin/js/users.js b/static/admin/js/users.js new file mode 100644 index 00000000..18dab0ea --- /dev/null +++ b/static/admin/js/users.js @@ -0,0 +1,140 @@ +function adminUsers() { + return { + // State + users: [], + loading: false, + filters: { + search: '', + role: '', + is_active: '' + }, + stats: { + total: 0, + active: 0, + vendors: 0, + admins: 0 + }, + pagination: { + page: 1, + per_page: 10, + total: 0, + pages: 0 + }, + + // Initialization + init() { + Logger.info('Users page initialized', 'USERS'); + this.loadUsers(); + this.loadStats(); + }, + + // Load users from API + async loadUsers() { + this.loading = true; + try { + const params = new URLSearchParams({ + page: this.pagination.page, + per_page: this.pagination.per_page, + ...this.filters + }); + + const response = await ApiClient.get(`/admin/users?${params}`); + + if (response.items) { + this.users = response.items; + this.pagination.total = response.total; + this.pagination.pages = response.pages; + } + } catch (error) { + Logger.error('Failed to load users', 'USERS', error); + Utils.showToast('Failed to load users', 'error'); + } finally { + this.loading = false; + } + }, + + // Load statistics + async loadStats() { + try { + const response = await ApiClient.get('/admin/users/stats'); + if (response) { + this.stats = response; + } + } catch (error) { + Logger.error('Failed to load stats', 'USERS', error); + } + }, + + // Search with debounce + debouncedSearch: Utils.debounce(function() { + this.pagination.page = 1; + this.loadUsers(); + }, 500), + + // Pagination + nextPage() { + if (this.pagination.page < this.pagination.pages) { + this.pagination.page++; + this.loadUsers(); + } + }, + + previousPage() { + if (this.pagination.page > 1) { + this.pagination.page--; + this.loadUsers(); + } + }, + + // Actions + viewUser(user) { + Logger.info('View user', 'USERS', user); + // TODO: Open view modal + }, + + editUser(user) { + Logger.info('Edit user', 'USERS', user); + // TODO: Open edit modal + }, + + async toggleUserStatus(user) { + const action = user.is_active ? 'deactivate' : 'activate'; + if (!confirm(`Are you sure you want to ${action} ${user.username}?`)) { + return; + } + + try { + await ApiClient.put(`/admin/users/${user.id}/status`, { + is_active: !user.is_active + }); + Utils.showToast(`User ${action}d successfully`, 'success'); + this.loadUsers(); + this.loadStats(); + } catch (error) { + Logger.error(`Failed to ${action} user`, 'USERS', error); + Utils.showToast(`Failed to ${action} user`, 'error'); + } + }, + + async deleteUser(user) { + if (!confirm(`Are you sure you want to delete ${user.username}? This action cannot be undone.`)) { + return; + } + + try { + await ApiClient.delete(`/admin/users/${user.id}`); + Utils.showToast('User deleted successfully', 'success'); + this.loadUsers(); + this.loadStats(); + } catch (error) { + Logger.error('Failed to delete user', 'USERS', error); + Utils.showToast('Failed to delete user', 'error'); + } + }, + + openCreateModal() { + Logger.info('Open create user modal', 'USERS'); + // TODO: Open create modal + } + }; +} \ No newline at end of file diff --git a/static/admin/js/vendor-edit.js b/static/admin/js/vendor-edit.js index dbdcdf18..ce3f2d99 100644 --- a/static/admin/js/vendor-edit.js +++ b/static/admin/js/vendor-edit.js @@ -1,338 +1,206 @@ -// static/js/admin/vendor-edit.js +// static/admin/js/vendor-edit.js -function vendorEdit() { +// Log levels: 0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug +const VENDOR_EDIT_LOG_LEVEL = 3; + +const editLog = { + error: (...args) => VENDOR_EDIT_LOG_LEVEL >= 1 && console.error('❌ [VENDOR_EDIT ERROR]', ...args), + warn: (...args) => VENDOR_EDIT_LOG_LEVEL >= 2 && console.warn('⚠️ [VENDOR_EDIT WARN]', ...args), + info: (...args) => VENDOR_EDIT_LOG_LEVEL >= 3 && console.info('ℹ️ [VENDOR_EDIT INFO]', ...args), + debug: (...args) => VENDOR_EDIT_LOG_LEVEL >= 4 && console.log('🔍 [VENDOR_EDIT DEBUG]', ...args) +}; + +function adminVendorEdit() { return { - currentUser: {}, - vendor: {}, + // Inherit base layout functionality from init-alpine.js + ...data(), + + // Vendor edit page specific state + currentPage: 'vendor-edit', + vendor: null, formData: {}, errors: {}, - loadingVendor: true, + loadingVendor: false, saving: false, - vendorId: null, + vendorCode: null, - // Confirmation modal - confirmModal: { - show: false, - title: '', - message: '', - warning: '', - buttonText: '', - buttonClass: 'btn-primary', - onConfirm: () => {}, - onCancel: null - }, + // Initialize + async init() { + editLog.info('=== VENDOR EDIT PAGE INITIALIZING ==='); - // Success modal - successModal: { - show: false, - title: '', - message: '', - details: null, - note: '' - }, - - // Transfer ownership - showTransferOwnership: false, - transferring: false, - transferData: { - new_owner_user_id: null, - transfer_reason: '', - confirm_transfer: false - }, - - init() { - console.log('=== Vendor Edit Initialization ==='); - - // Check authentication - if (!Auth.isAuthenticated() || !Auth.isAdmin()) { - console.log('Not authenticated as admin, redirecting to login'); - window.location.href = '/admin/login'; + // Prevent multiple initializations + if (window._vendorEditInitialized) { + editLog.warn('Vendor edit page already initialized, skipping...'); return; } + window._vendorEditInitialized = true; - this.currentUser = Auth.getCurrentUser(); - console.log('Current user:', this.currentUser.username); + // Get vendor code from URL + const path = window.location.pathname; + const match = path.match(/\/admin\/vendors\/([^\/]+)\/edit/); - // Get vendor ID from URL - const urlParams = new URLSearchParams(window.location.search); - this.vendorId = urlParams.get('id'); - - if (!this.vendorId) { - console.error('No vendor ID in URL'); - alert('No vendor ID provided'); - window.location.href = '/admin/dashboard.html#vendors'; - return; + if (match) { + this.vendorCode = match[1]; + editLog.info('Editing vendor:', this.vendorCode); + await this.loadVendor(); + } else { + editLog.error('No vendor code in URL'); + Utils.showToast('Invalid vendor URL', 'error'); + setTimeout(() => window.location.href = '/admin/vendors', 2000); } - console.log('Vendor ID:', this.vendorId); - - // Load vendor details - this.loadVendor(); + editLog.info('=== VENDOR EDIT PAGE INITIALIZATION COMPLETE ==='); }, + // Load vendor data async loadVendor() { + editLog.info('Loading vendor data...'); this.loadingVendor = true; - try { - console.log('Loading vendor with ID:', this.vendorId); - this.vendor = await apiClient.get(`/admin/vendors/${this.vendorId}`); - console.log('✅ Vendor loaded:', this.vendor.vendor_code); - console.log('Owner email:', this.vendor.owner_email); - console.log('Contact email:', this.vendor.contact_email); - // Populate form data + try { + const startTime = Date.now(); + const response = await apiClient.get(`/admin/vendors/${this.vendorCode}`); + const duration = Date.now() - startTime; + + this.vendor = response; + + // Initialize form data this.formData = { - name: this.vendor.name, - subdomain: this.vendor.subdomain, - description: this.vendor.description || '', - contact_email: this.vendor.contact_email || '', - contact_phone: this.vendor.contact_phone || '', - website: this.vendor.website || '', - business_address: this.vendor.business_address || '', - tax_number: this.vendor.tax_number || '' + name: response.name || '', + subdomain: response.subdomain || '', + 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 || '' }; - console.log('Form data populated'); + editLog.info(`Vendor loaded in ${duration}ms`, { + vendor_code: this.vendor.vendor_code, + name: this.vendor.name + }); + editLog.debug('Form data initialized:', this.formData); + } catch (error) { - console.error('❌ Failed to load vendor:', error); - Utils.showToast('Failed to load vendor details: ' + (error.message || 'Unknown error'), 'error'); - window.location.href = '/admin/dashboard'; + editLog.error('Failed to load vendor:', error); + Utils.showToast('Failed to load vendor', 'error'); + setTimeout(() => window.location.href = '/admin/vendors', 2000); } finally { this.loadingVendor = false; } }, + // Format subdomain formatSubdomain() { this.formData.subdomain = this.formData.subdomain .toLowerCase() .replace(/[^a-z0-9-]/g, ''); + editLog.debug('Subdomain formatted:', this.formData.subdomain); }, + // Submit form async handleSubmit() { - console.log('Submitting vendor update...'); + editLog.info('=== SUBMITTING VENDOR UPDATE ==='); + editLog.debug('Form data:', this.formData); + this.errors = {}; this.saving = true; try { - const updatedVendor = await apiClient.put( - `/admin/vendors/${this.vendorId}`, + const startTime = Date.now(); + const response = await apiClient.put( + `/admin/vendors/${this.vendorCode}`, this.formData ); + const duration = Date.now() - startTime; - console.log('✅ Vendor updated successfully'); - Utils.showToast('Vendor updated successfully!', 'success'); - this.vendor = updatedVendor; + this.vendor = response; + Utils.showToast('Vendor updated successfully', 'success'); + editLog.info(`Vendor updated successfully in ${duration}ms`, response); + + // Optionally redirect back to list + // setTimeout(() => window.location.href = '/admin/vendors', 1500); - // Refresh form data with latest values - this.formData.name = updatedVendor.name; - this.formData.subdomain = updatedVendor.subdomain; - this.formData.contact_email = updatedVendor.contact_email; } catch (error) { - console.error('❌ Failed to update vendor:', error); + editLog.error('Failed to update vendor:', error); + + // 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; + } + }); + editLog.debug('Validation errors:', this.errors); + } + Utils.showToast(error.message || 'Failed to update vendor', 'error'); } finally { this.saving = false; + editLog.info('=== VENDOR UPDATE COMPLETE ==='); } }, - showVerificationModal() { - const action = this.vendor.is_verified ? 'unverify' : 'verify'; - const actionCap = this.vendor.is_verified ? 'Unverify' : 'Verify'; - - this.confirmModal = { - show: true, - title: `${actionCap} Vendor`, - message: `Are you sure you want to ${action} this vendor?`, - warning: this.vendor.is_verified - ? 'Unverifying this vendor will prevent them from being publicly visible and may affect their operations.' - : 'Verifying this vendor will make them publicly visible and allow them to operate fully.', - buttonText: actionCap, - buttonClass: this.vendor.is_verified ? 'btn-warning' : 'btn-success', - onConfirm: () => this.toggleVerification(), - onCancel: null - }; - }, - + // Toggle verification async toggleVerification() { const action = this.vendor.is_verified ? 'unverify' : 'verify'; - console.log(`Toggling verification: ${action}`); + editLog.info(`Toggle verification: ${action}`); + + if (!confirm(`Are you sure you want to ${action} this vendor?`)) { + editLog.info('Verification toggle cancelled by user'); + return; + } this.saving = true; try { - const result = await apiClient.put(`/admin/vendors/${this.vendorId}/verify`); - this.vendor.is_verified = result.vendor.is_verified; - console.log('✅ Verification toggled'); - Utils.showToast(result.message, 'success'); + const response = await apiClient.put( + `/admin/vendors/${this.vendorCode}/verification`, + { is_verified: !this.vendor.is_verified } + ); + + this.vendor = response; + Utils.showToast(`Vendor ${action}ed successfully`, 'success'); + editLog.info(`Vendor ${action}ed successfully`); + } catch (error) { - console.error('❌ Failed to toggle verification:', error); - Utils.showToast('Failed to update verification status', 'error'); + editLog.error(`Failed to ${action} vendor:`, error); + Utils.showToast(`Failed to ${action} vendor`, 'error'); } finally { this.saving = false; } }, - showStatusModal() { + // Toggle active status + async toggleActive() { const action = this.vendor.is_active ? 'deactivate' : 'activate'; - const actionCap = this.vendor.is_active ? 'Deactivate' : 'Activate'; + editLog.info(`Toggle active status: ${action}`); - this.confirmModal = { - show: true, - title: `${actionCap} Vendor`, - message: `Are you sure you want to ${action} this vendor?`, - warning: this.vendor.is_active - ? 'Deactivating this vendor will immediately suspend all their operations and make them invisible to customers.' - : 'Activating this vendor will restore their operations and make them visible again.', - buttonText: actionCap, - buttonClass: this.vendor.is_active ? 'btn-danger' : 'btn-success', - onConfirm: () => this.toggleStatus(), - onCancel: null - }; - }, - - async toggleStatus() { - const action = this.vendor.is_active ? 'deactivate' : 'activate'; - console.log(`Toggling status: ${action}`); + if (!confirm(`Are you sure you want to ${action} this vendor?\n\nThis will affect their operations.`)) { + editLog.info('Active status toggle cancelled by user'); + return; + } this.saving = true; try { - const result = await apiClient.put(`/admin/vendors/${this.vendorId}/status`); - this.vendor.is_active = result.vendor.is_active; - console.log('✅ Status toggled'); - Utils.showToast(result.message, 'success'); + const response = await apiClient.put( + `/admin/vendors/${this.vendorCode}/status`, + { is_active: !this.vendor.is_active } + ); + + this.vendor = response; + Utils.showToast(`Vendor ${action}d successfully`, 'success'); + editLog.info(`Vendor ${action}d successfully`); + } catch (error) { - console.error('❌ Failed to toggle status:', error); - Utils.showToast('Failed to update vendor status', 'error'); + editLog.error(`Failed to ${action} vendor:`, error); + Utils.showToast(`Failed to ${action} vendor`, 'error'); } finally { this.saving = false; } - }, - - async handleTransferOwnership() { - // Validate inputs - if (!this.transferData.confirm_transfer) { - Utils.showToast('Please confirm the ownership transfer', 'error'); - return; - } - - if (!this.transferData.new_owner_user_id) { - Utils.showToast('Please enter the new owner user ID', 'error'); - return; - } - - // Close the transfer modal first - this.showTransferOwnership = false; - - // Wait a moment for modal to close - await new Promise(resolve => setTimeout(resolve, 300)); - - // Show final confirmation modal - this.confirmModal = { - show: true, - title: '⚠️ FINAL CONFIRMATION: Transfer Ownership', - message: `You are about to transfer ownership of "${this.vendor.name}" to user ID ${this.transferData.new_owner_user_id}.`, - warning: `Current Owner: ${this.vendor.owner_username} (${this.vendor.owner_email})\n\n` + - `This action will:\n` + - `• Assign full ownership rights to the new user\n` + - `• Demote the current owner to Manager role\n` + - `• Be permanently logged for audit purposes\n\n` + - `This action cannot be easily undone. Are you absolutely sure?`, - buttonText: '🔄 Yes, Transfer Ownership', - buttonClass: 'btn-danger', - onConfirm: () => this.executeTransferOwnership(), - onCancel: () => { - // If cancelled, reopen the transfer modal with preserved data - this.showTransferOwnership = true; - } - }; - }, - - async executeTransferOwnership() { - console.log('Transferring ownership to user:', this.transferData.new_owner_user_id); - this.transferring = true; - this.saving = true; - - try { - const result = await apiClient.post( - `/admin/vendors/${this.vendorId}/transfer-ownership`, - this.transferData - ); - - console.log('✅ Ownership transferred successfully'); - - // Show beautiful success modal - this.successModal = { - show: true, - title: 'Ownership Transfer Complete', - message: `The ownership of "${this.vendor.name}" has been successfully transferred.`, - details: { - oldOwner: { - username: result.old_owner.username, - email: result.old_owner.email - }, - newOwner: { - username: result.new_owner.username, - email: result.new_owner.email - } - }, - note: 'The transfer has been logged for audit purposes. The previous owner has been assigned the Manager role.' - }; - - Utils.showToast('Ownership transferred successfully', 'success'); - - // Reload vendor data to reflect new owner - await this.loadVendor(); - - // Reset transfer form data - this.transferData = { - new_owner_user_id: null, - transfer_reason: '', - confirm_transfer: false - }; - } catch (error) { - console.error('❌ Failed to transfer ownership:', error); - const errorMsg = error.message || error.detail || 'Unknown error'; - Utils.showToast(`Transfer failed: ${errorMsg}`, 'error'); - - // Show error in modal format (reuse success modal structure) - alert(`❌ Transfer Failed\n\n${errorMsg}\n\nPlease check the user ID and try again.`); - - // Reopen transfer modal so user can try again - this.showTransferOwnership = true; - } finally { - this.transferring = false; - this.saving = false; - } -}, - - async handleLogout() { - // Show confirmation modal for logout - this.confirmModal = { - show: true, - title: '🚪 Confirm Logout', - message: 'Are you sure you want to logout from the Admin Portal?', - warning: 'You will need to login again to access the admin dashboard.', - buttonText: 'Yes, Logout', - buttonClass: 'btn-danger', - onConfirm: () => this.executeLogout(), - onCancel: null + } }; -}, +} - async executeLogout() { - console.log('Logging out...'); - - // Show loading state briefly - this.saving = true; - - // Clear authentication - Auth.logout(); - - // Show success message - Utils.showToast('Logged out successfully', 'success', 1000); - - // Redirect to login after brief delay - setTimeout(() => { - window.location.href = '/admin/login'; - }, 500); -}, - }; -} \ No newline at end of file +editLog.info('Vendor edit module loaded'); \ No newline at end of file diff --git a/static/admin/js/vendors.js b/static/admin/js/vendors.js index 635af7bd..3bb5c88a 100644 --- a/static/admin/js/vendors.js +++ b/static/admin/js/vendors.js @@ -1,247 +1,258 @@ // static/admin/js/vendors.js -// Admin Vendor Creation Component -function vendorCreation() { + +// Log levels: 0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug +const VENDORS_LOG_LEVEL = 3; + +const vendorsLog = { + error: (...args) => VENDORS_LOG_LEVEL >= 1 && console.error('❌ [VENDORS ERROR]', ...args), + warn: (...args) => VENDORS_LOG_LEVEL >= 2 && console.warn('⚠️ [VENDORS WARN]', ...args), + info: (...args) => VENDORS_LOG_LEVEL >= 3 && console.info('ℹ️ [VENDORS INFO]', ...args), + debug: (...args) => VENDORS_LOG_LEVEL >= 4 && console.log('🔍 [VENDORS DEBUG]', ...args) +}; + +// ============================================ +// VENDOR LIST FUNCTION +// ============================================ +function adminVendors() { return { - currentUser: {}, - formData: { - vendor_code: '', - name: '', - subdomain: '', - description: '', - owner_email: '', - business_phone: '', - website: '', - business_address: '', - tax_number: '' + // Inherit base layout functionality from init-alpine.js + ...data(), + + // Vendors page specific state + currentPage: 'vendors', + vendors: [], + stats: { + total: 0, + verified: 0, + pending: 0, + inactive: 0 }, loading: false, - errors: {}, - showCredentials: false, - credentials: null, + error: null, - init() { - if (!this.checkAuth()) { + // Pagination state + currentPage: 1, + itemsPerPage: 10, + + // Initialize + async init() { + vendorsLog.info('=== VENDORS PAGE INITIALIZING ==='); + + // Prevent multiple initializations + if (window._vendorsInitialized) { + vendorsLog.warn('Vendors page already initialized, skipping...'); return; } + window._vendorsInitialized = true; + + await this.loadVendors(); + await this.loadStats(); + + vendorsLog.info('=== VENDORS PAGE INITIALIZATION COMPLETE ==='); }, - checkAuth() { - if (!Auth.isAuthenticated()) { - // ← CHANGED: Use new Jinja2 route - window.location.href = '/admin/login'; - return false; - } - - const user = Auth.getCurrentUser(); - if (!user || user.role !== 'admin') { - Utils.showToast('Access denied. Admin privileges required.', 'error'); - Auth.logout(); - // ← CHANGED: Use new Jinja2 route - window.location.href = '/admin/login'; - return false; - } - - this.currentUser = user; - return true; + // Computed: Get paginated vendors for current page + get paginatedVendors() { + const start = (this.currentPage - 1) * this.itemsPerPage; + const end = start + this.itemsPerPage; + return this.vendors.slice(start, end); }, - async handleLogout() { - const confirmed = await Utils.confirm( - 'Are you sure you want to logout?', - 'Confirm Logout' - ); - - if (confirmed) { - Auth.logout(); - Utils.showToast('Logged out successfully', 'success', 2000); - setTimeout(() => { - // ← CHANGED: Use new Jinja2 route - window.location.href = '/admin/login'; - }, 500); - } + // Computed: Total number of pages + get totalPages() { + return Math.ceil(this.vendors.length / this.itemsPerPage); }, - // ... rest of the methods stay the same ... - - // Auto-format vendor code (uppercase) - formatVendorCode() { - this.formData.vendor_code = this.formData.vendor_code - .toUpperCase() - .replace(/[^A-Z0-9_-]/g, ''); + // Computed: Start index for pagination display + get startIndex() { + if (this.vendors.length === 0) return 0; + return (this.currentPage - 1) * this.itemsPerPage + 1; }, - // Auto-format subdomain (lowercase) - formatSubdomain() { - this.formData.subdomain = this.formData.subdomain - .toLowerCase() - .replace(/[^a-z0-9-]/g, ''); + // Computed: End index for pagination display + get endIndex() { + const end = this.currentPage * this.itemsPerPage; + return end > this.vendors.length ? this.vendors.length : end; }, - clearErrors() { - this.errors = {}; + // Computed: Generate page numbers array with ellipsis + get pageNumbers() { + const pages = []; + const totalPages = this.totalPages; + const current = this.currentPage; + + if (totalPages <= 7) { + // Show all pages if 7 or fewer + for (let i = 1; i <= totalPages; i++) { + pages.push(i); + } + } else { + // Always show first page + pages.push(1); + + if (current > 3) { + pages.push('...'); + } + + // Show pages around current page + 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('...'); + } + + // Always show last page + pages.push(totalPages); + } + + return pages; }, - validateForm() { - this.clearErrors(); - let isValid = true; - - // Required fields validation - if (!this.formData.vendor_code.trim()) { - this.errors.vendor_code = 'Vendor code is required'; - isValid = false; - } - - if (!this.formData.name.trim()) { - this.errors.name = 'Vendor name is required'; - isValid = false; - } - - if (!this.formData.subdomain.trim()) { - this.errors.subdomain = 'Subdomain is required'; - isValid = false; - } - - if (!this.formData.owner_email.trim()) { - this.errors.owner_email = 'Owner email is required'; - isValid = false; - } - - // Email validation - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (this.formData.owner_email && !emailRegex.test(this.formData.owner_email)) { - this.errors.owner_email = 'Invalid email format'; - isValid = false; - } - - // Subdomain validation (must start and end with alphanumeric) - const subdomainRegex = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/; - if (this.formData.subdomain && this.formData.subdomain.length > 1 && - !subdomainRegex.test(this.formData.subdomain)) { - this.errors.subdomain = 'Subdomain must start and end with a letter or number'; - isValid = false; - } - - return isValid; - }, - - async handleSubmit() { - if (!this.validateForm()) { - Utils.showToast('Please fix validation errors', 'error'); - window.scrollTo({ top: 0, behavior: 'smooth' }); - return; - } - + // Load vendors list + async loadVendors() { + vendorsLog.info('Loading vendors list...'); this.loading = true; + this.error = null; try { - // Prepare data (remove empty fields) - const submitData = {}; - for (const [key, value] of Object.entries(this.formData)) { - if (value !== '' && value !== null && value !== undefined) { - submitData[key] = value; - } + const startTime = Date.now(); + const response = await apiClient.get('/admin/vendors'); + const duration = Date.now() - startTime; + + // Handle different response structures + this.vendors = response.vendors || response.items || response || []; + + vendorsLog.info(`Vendors loaded in ${duration}ms`, { + count: this.vendors.length, + hasVendors: this.vendors.length > 0 + }); + + if (this.vendors.length > 0) { + vendorsLog.debug('First vendor:', this.vendors[0]); } - console.log('Submitting vendor data:', submitData); - - const response = await apiClient.post('/admin/vendors', submitData); - - console.log('Vendor creation response:', response); - - // Store credentials - be flexible with response structure - this.credentials = { - vendor_code: response.vendor_code || this.formData.vendor_code, - subdomain: response.subdomain || this.formData.subdomain, - name: response.name || this.formData.name, - owner_username: response.owner_username || `${this.formData.subdomain}_owner`, - owner_email: response.owner_email || this.formData.owner_email, - temporary_password: response.temporary_password || 'PASSWORD_NOT_RETURNED', - login_url: response.login_url || - `http://localhost:8000/vendor/${this.formData.subdomain}/login` || - `${this.formData.subdomain}.platform.com/vendor/login` - }; - - console.log('Stored credentials:', this.credentials); - - // Check if password was returned - if (!response.temporary_password) { - console.warn('⚠️ Warning: temporary_password not returned from API'); - console.warn('Full API response:', response); - Utils.showToast('Vendor created but password not returned. Check server logs.', 'warning', 5000); - } - - // Show credentials display - this.showCredentials = true; - - // Success notification - Utils.showToast('Vendor created successfully!', 'success'); - - // Scroll to top to see credentials - window.scrollTo({ top: 0, behavior: 'smooth' }); + // Reset to first page when data is loaded + this.currentPage = 1; } catch (error) { - console.error('Error creating vendor:', error); - - // Check for specific validation errors - if (error.message.includes('vendor_code') || error.message.includes('Vendor code')) { - this.errors.vendor_code = 'Vendor code already exists'; - } else if (error.message.includes('subdomain')) { - this.errors.subdomain = 'Subdomain already exists'; - } else if (error.message.includes('email')) { - this.errors.owner_email = 'Email already in use'; - } - - Utils.showToast( - error.message || 'Failed to create vendor', - 'error' - ); + vendorsLog.error('Failed to load vendors:', error); + this.error = error.message || 'Failed to load vendors'; + Utils.showToast('Failed to load vendors', 'error'); } finally { this.loading = false; } }, - resetForm() { - this.formData = { - vendor_code: '', - name: '', - subdomain: '', - description: '', - owner_email: '', - business_phone: '', - website: '', - business_address: '', - tax_number: '' - }; - this.clearErrors(); - this.showCredentials = false; - this.credentials = null; + // Load statistics + async loadStats() { + vendorsLog.info('Loading vendor statistics...'); + + try { + const startTime = Date.now(); + const response = await apiClient.get('/admin/vendors/stats'); + const duration = Date.now() - startTime; + + this.stats = response; + vendorsLog.info(`Stats loaded in ${duration}ms`, this.stats); + + } catch (error) { + vendorsLog.error('Failed to load stats:', error); + // Don't show error toast for stats, just log it + } }, - copyToClipboard(text, label) { - if (!text) { - Utils.showToast('Nothing to copy', 'error'); + // Pagination: Go to specific page + goToPage(page) { + if (page === '...' || page < 1 || page > this.totalPages) { + return; + } + vendorsLog.info('Going to page:', page); + this.currentPage = page; + }, + + // Pagination: Go to next page + nextPage() { + if (this.currentPage < this.totalPages) { + vendorsLog.info('Going to next page'); + this.currentPage++; + } + }, + + // Pagination: Go to previous page + previousPage() { + if (this.currentPage > 1) { + vendorsLog.info('Going to previous page'); + this.currentPage--; + } + }, + + // Format date (matches dashboard pattern) + formatDate(dateString) { + if (!dateString) { + vendorsLog.debug('formatDate called with empty dateString'); + return '-'; + } + const formatted = Utils.formatDate(dateString); + vendorsLog.debug(`Date formatted: ${dateString} -> ${formatted}`); + return formatted; + }, + + // View vendor details + viewVendor(vendorCode) { + vendorsLog.info('Navigating to vendor details:', vendorCode); + const url = `/admin/vendors/${vendorCode}`; + vendorsLog.debug('Navigation URL:', url); + window.location.href = url; + }, + + // Edit vendor + editVendor(vendorCode) { + vendorsLog.info('Navigating to vendor edit:', vendorCode); + const url = `/admin/vendors/${vendorCode}/edit`; + vendorsLog.debug('Navigation URL:', url); + window.location.href = url; + }, + + // Delete vendor + async deleteVendor(vendor) { + vendorsLog.info('Delete vendor requested:', vendor.vendor_code); + + if (!confirm(`Are you sure you want to delete vendor "${vendor.name}"?\n\nThis action cannot be undone.`)) { + vendorsLog.info('Delete cancelled by user'); return; } - navigator.clipboard.writeText(text).then(() => { - Utils.showToast(`${label} copied to clipboard`, 'success', 2000); - }).catch((err) => { - console.error('Failed to copy:', err); - // Fallback for older browsers - const textArea = document.createElement('textarea'); - textArea.value = text; - textArea.style.position = 'fixed'; - textArea.style.left = '-999999px'; - document.body.appendChild(textArea); - textArea.select(); - try { - document.execCommand('copy'); - Utils.showToast(`${label} copied to clipboard`, 'success', 2000); - } catch (err) { - Utils.showToast('Failed to copy to clipboard', 'error'); - } - document.body.removeChild(textArea); - }); + try { + vendorsLog.info('Deleting vendor:', vendor.vendor_code); + await apiClient.delete(`/admin/vendors/${vendor.vendor_code}`); + + Utils.showToast('Vendor deleted successfully', 'success'); + vendorsLog.info('Vendor deleted successfully'); + + // Reload data + await this.loadVendors(); + await this.loadStats(); + + } catch (error) { + vendorsLog.error('Failed to delete vendor:', error); + Utils.showToast(error.message || 'Failed to delete vendor', 'error'); + } + }, + + // Refresh vendors list + async refresh() { + vendorsLog.info('=== VENDORS REFRESH TRIGGERED ==='); + await this.loadVendors(); + await this.loadStats(); + Utils.showToast('Vendors list refreshed', 'success'); + vendorsLog.info('=== VENDORS REFRESH COMPLETE ==='); } - } -} \ No newline at end of file + }; +} + +vendorsLog.info('Vendors module loaded'); \ No newline at end of file diff --git a/static/admin/login.html b/static/admin/login.html deleted file mode 100644 index a1b638c8..00000000 --- a/static/admin/login.html +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - Admin Login - Multi-Tenant Platform - - - - - -
    -
    -
    -
    - - -
    -
    -
    -

    - Admin Login -

    - - -
    - -
    - - -
    - - - - - -
    - -
    - -

    - - Forgot your password? - -

    -

    - - ← Back to Platform - -

    -
    -
    -
    -
    -
    - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/static/admin/oldlogin.html b/static/admin/oldlogin.html deleted file mode 100644 index b91bda34..00000000 --- a/static/admin/oldlogin.html +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - Admin Login - Multi-Tenant Ecommerce Platform - - - - - -
    -
    - - - -
    - -
    - - -
    -
    - - -
    -
    - -
    - - -
    -
    - - -
    - - -
    -
    - - - - - \ No newline at end of file diff --git a/static/js/admin/admin-layout-templates.js b/static/js/admin/admin-layout-templates.js deleted file mode 100644 index e40167f7..00000000 --- a/static/js/admin/admin-layout-templates.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Admin Layout Templates - * Header and Sidebar specific to Admin Portal - */ - -window.adminLayoutTemplates = { - - /** - * Admin Header - */ - header: () => ` -
    -
    - -

    Admin Portal

    -
    -
    - - -
    -
    - `, - - /** - * Admin Sidebar - */ - sidebar: () => ` - - ` -}; \ No newline at end of file diff --git a/static/shared/js/api-client.js b/static/shared/js/api-client.js index 6955c22b..6d87215a 100644 --- a/static/shared/js/api-client.js +++ b/static/shared/js/api-client.js @@ -330,108 +330,6 @@ const Auth = { } }; -/** - * Utility functions - */ -const Utils = { - /** - * Format date - */ - formatDate(dateString) { - if (!dateString) return '-'; - const date = new Date(dateString); - return date.toLocaleDateString('en-GB', { - day: '2-digit', - month: 'short', - year: 'numeric' - }); - }, - - /** - * Format datetime - */ - formatDateTime(dateString) { - if (!dateString) return '-'; - const date = new Date(dateString); - return date.toLocaleString('en-GB', { - day: '2-digit', - month: 'short', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - }, - - /** - * Format currency - */ - formatCurrency(amount, currency = 'EUR') { - if (amount === null || amount === undefined) return '-'; - return new Intl.NumberFormat('en-GB', { - style: 'currency', - currency: currency - }).format(amount); - }, - - /** - * Debounce function - */ - debounce(func, wait) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; - }, - - /** - * Show toast notification - */ - showToast(message, type = 'info', duration = 3000) { - apiLog.debug('Showing toast:', { message, type, duration }); - - // Create toast element - const toast = document.createElement('div'); - toast.className = `toast toast-${type}`; - toast.textContent = message; - - // Style - toast.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - padding: 16px 24px; - background: ${type === 'success' ? '#4caf50' : type === 'error' ? '#f44336' : '#2196f3'}; - color: white; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0,0,0,0.15); - z-index: 10000; - animation: slideIn 0.3s ease; - max-width: 400px; - `; - - // Add to page - document.body.appendChild(toast); - - // Remove after duration - setTimeout(() => { - toast.style.animation = 'slideOut 0.3s ease'; - setTimeout(() => toast.remove(), 300); - }, duration); - }, - - /** - * Confirm dialog - */ - async confirm(message, title = 'Confirm') { - return window.confirm(`${title}\n\n${message}`); - } -}; - // Add animation styles const style = document.createElement('style'); style.textContent = ` diff --git a/static/shared/js/icons.js b/static/shared/js/icons.js index be669b47..0f8257d6 100644 --- a/static/shared/js/icons.js +++ b/static/shared/js/icons.js @@ -1,336 +1,114 @@ +// static/shared/js/icons.js /** - * Heroicons Helper - Inline SVG Icons - * Usage: icon('home') or icon('home', 'w-6 h-6') + * Heroicons Icon System + * Inline SVG icons with Alpine.js magic helper */ +/** + * Icon definitions (Heroicons outline style) + * Each icon is an SVG template with {{classes}} placeholder + */ const Icons = { // Navigation - home: ` - - `, - - menu: ` - - `, - - search: ` - - `, + 'home': ``, + 'menu': ``, + 'search': ``, + 'arrow-left': ``, + 'chevron-down': ``, + 'chevron-right': ``, // User & Profile - user: ` - - `, - - users: ` - - `, + 'user': ``, + 'users': ``, + 'user-circle': ``, + 'user-group': ``, + 'identification': ``, + 'badge-check': ``, // Actions - edit: ` - - `, + 'edit': ``, + 'delete': ``, + 'plus': ``, + 'check': ``, + 'close': ``, + 'refresh': ``, + 'duplicate': ``, + 'eye': ``, + 'eye-off': ``, + 'filter': ``, + 'dots-vertical': ``, + 'dots-horizontal': ``, - delete: ` - - `, + // E-commerce + 'shopping-bag': ``, + 'shopping-cart': ``, + 'credit-card': ``, + 'currency-dollar': ``, + 'gift': ``, + 'tag': ``, + 'truck': ``, + 'receipt': ``, - plus: ` - - `, + // Inventory & Products + 'cube': ``, + 'collection': ``, + 'photograph': ``, + 'color-swatch': ``, + 'template': ``, + 'clipboard-list': ``, - check: ` - - `, + // Analytics & Reports + 'chart': ``, + 'trending-up': ``, + 'trending-down': ``, + 'presentation-chart-line': ``, + 'calculator': ``, - close: ` - - `, + // Communication + 'bell': ``, + 'mail': ``, + 'chat': ``, + 'annotation': ``, + 'phone': ``, - // Theme & Settings - sun: ` - - `, + // System & Settings + 'cog': ``, + 'sun': ``, + 'moon': ``, + 'database': ``, + 'server': ``, + 'shield-check': ``, + 'key': ``, + 'lock-closed': ``, + 'lock-open': ``, - moon: ` - - `, + // Document & File + 'document': ``, + 'folder': ``, + 'folder-open': ``, + 'download': ``, + 'upload': ``, - cog: ` - - - `, + // Time & Calendar + 'calendar': ``, + 'clock': ``, - // Notifications & Communication - bell: ` - - `, - - mail: ` - - - `, - - // Logout - logout: ` - - `, - - // Business/Commerce - 'shopping-bag': ` - - `, - - cube: ` - - `, - - chart: ` - - `, - - // Arrows & Directions - 'chevron-down': ` - - `, - - 'chevron-right': ` - - `, - - 'arrow-left': ` - - `, + // Location + 'location-marker': ``, + 'globe': ``, // Status & Indicators - 'exclamation': ` - - `, + 'exclamation': ``, + 'information-circle': ``, + 'spinner': ``, + 'star': ``, + 'heart': ``, + 'flag': ``, - 'information-circle': ` - - `, - - // Loading - spinner: ` - - - `, - - // E-commerce Specific - 'shopping-cart': ` - - `, - - 'credit-card': ` - - `, - - 'currency-dollar': ` - - `, - - 'gift': ` - - `, - - 'tag': ` - - `, - - 'truck': ` - - - `, - - 'receipt': ` - - `, - - 'clipboard-list': ` - - `, - - // Inventory & Products - 'collection': ` - - `, - - 'photograph': ` - - `, - - 'color-swatch': ` - - `, - - 'template': ` - - `, - - // Analytics & Reports - 'trending-up': ` - - `, - - 'trending-down': ` - - `, - - 'presentation-chart-line': ` - - `, - - 'calculator': ` - - `, - - // Customer Management - 'user-circle': ` - - `, - - 'user-group': ` - - `, - - 'identification': ` - - `, - - 'badge-check': ` - - `, - - // Documents & Files - 'document': ` - - `, - - 'folder': ` - - `, - - 'folder-open': ` - - - `, - - 'download': ` - - `, - - 'upload': ` - - `, - - // Time & Calendar - 'calendar': ` - - `, - - 'clock': ` - - `, - - // System & Settings - 'database': ` - - `, - - 'server': ` - - `, - - 'shield-check': ` - - `, - - 'key': ` - - `, - - 'lock-closed': ` - - `, - - 'lock-open': ` - - `, - - // Actions & Interactions - 'refresh': ` - - `, - - 'duplicate': ` - - `, - - 'eye': ` - - - `, - - 'eye-off': ` - - `, - - 'filter': ` - - `, - - 'dots-vertical': ` - - `, - - 'dots-horizontal': ` - - `, - - // Communication - 'chat': ` - - `, - - 'annotation': ` - - `, - - 'phone': ` - - `, - - // Location - 'location-marker': ` - - `, - - 'globe': ` - - `, - // Links & External - 'external-link': ` - - `, - - 'link': ` - - `, - - // Status Badges - 'star': ` - - `, - - 'heart': ` - - `, - - 'flag': ` - - ` + 'external-link': ``, + 'link': ``, + 'logout': `` }; /** @@ -352,13 +130,12 @@ function icon(name, classes = 'w-5 h-5') { * Alpine.js magic helper * Usage in Alpine: x-html="$icon('home')" or x-html="$icon('home', 'w-6 h-6')" */ -if (typeof Alpine !== 'undefined') { - document.addEventListener('alpine:init', () => { - Alpine.magic('icon', () => { - return (name, classes) => icon(name, classes); - }); - }); -} +document.addEventListener('alpine:init', () => { + // ✅ CORRECT: Return the function directly, not wrapped in another function + Alpine.magic('icon', () => icon); + + console.log('✅ Alpine $icon magic helper registered'); +}); // Export for use in modules if (typeof module !== 'undefined' && module.exports) { @@ -369,38 +146,5 @@ if (typeof module !== 'undefined' && module.exports) { window.icon = icon; window.Icons = Icons; -/** - * Get icon SVG with custom classes - * @param {string} name - Icon name from Icons object - * @param {string} classes - Tailwind classes (default: 'w-5 h-5') - * @returns {string} SVG markup - */ -function icon(name, classes = 'w-5 h-5') { - const iconTemplate = Icons[name]; - if (!iconTemplate) { - console.warn(`Icon "${name}" not found`); - return ''; - } - return iconTemplate.replace('{{classes}}', classes); -} - -/** - * Alpine.js magic helper - * Usage in Alpine: x-html="$icon('home')" or x-html="$icon('home', 'w-6 h-6')" - */ -if (typeof Alpine !== 'undefined') { - document.addEventListener('alpine:init', () => { - Alpine.magic('icon', () => { - return (name, classes) => icon(name, classes); - }); - }); -} - -// Export for use in modules -if (typeof module !== 'undefined' && module.exports) { - module.exports = { icon, Icons }; -} - -// Make available globally -window.icon = icon; -window.Icons = Icons; \ No newline at end of file +console.log('📦 Icon system loaded'); +console.log('📊 Total icons available:', Object.keys(Icons).length); diff --git a/static/shared/js/partial-loader.js b/static/shared/js/partial-loader.js deleted file mode 100644 index c61e4b8b..00000000 --- a/static/shared/js/partial-loader.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Partial Loader for Alpine.js v3 - * Loads HTML partials before Alpine initializes - */ -class PartialLoader { - constructor(area) { - this.area = area; // 'admin', 'vendor', or 'shop' - this.baseUrl = `/static/${area}/partials/`; - } - - async load(containerId, partialName) { - try { - const response = await fetch(`${this.baseUrl}${partialName}`); - - if (!response.ok) { - console.warn(`Failed to load partial: ${partialName} (${response.status})`); - return false; - } - - const html = await response.text(); - const container = document.getElementById(containerId); - - if (container) { - container.innerHTML = html; - return true; - } else { - console.warn(`Container not found: ${containerId}`); - return false; - } - } catch (error) { - console.error(`Error loading partial ${partialName}:`, error); - return false; - } - } - - async loadAll(partials) { - const promises = Object.entries(partials).map( - ([containerId, partialName]) => this.load(containerId, partialName) - ); - - const results = await Promise.all(promises); - const successCount = results.filter(r => r).length; - - console.log(`Loaded ${successCount}/${results.length} partials successfully`); - - return results.every(r => r); - } -} - -// Auto-detect area from URL path -function getAreaFromPath() { - const path = window.location.pathname; - - if (path.includes('/admin/')) return 'admin'; - if (path.includes('/vendor/')) return 'vendor'; - if (path.includes('/shop/')) return 'shop'; - - return 'shop'; // default -} - -// Create global instance -window.PartialLoader = PartialLoader; -window.partialLoader = new PartialLoader(getAreaFromPath()); \ No newline at end of file