diff --git a/app/routes/vendor_pages.py b/app/routes/vendor_pages.py
index 09005187..2c3f0159 100644
--- a/app/routes/vendor_pages.py
+++ b/app/routes/vendor_pages.py
@@ -527,6 +527,60 @@ async def vendor_billing_page(
)
+# ============================================================================
+# NOTIFICATIONS
+# ============================================================================
+
+
+@router.get(
+ "/{vendor_code}/notifications", response_class=HTMLResponse, include_in_schema=False
+)
+async def vendor_notifications_page(
+ request: Request,
+ vendor_code: str = Path(..., description="Vendor code"),
+ current_user: User = Depends(get_current_vendor_from_cookie_or_header),
+):
+ """
+ Render notifications center page.
+ JavaScript loads notifications via API.
+ """
+ return templates.TemplateResponse(
+ "vendor/notifications.html",
+ {
+ "request": request,
+ "user": current_user,
+ "vendor_code": vendor_code,
+ },
+ )
+
+
+# ============================================================================
+# ANALYTICS
+# ============================================================================
+
+
+@router.get(
+ "/{vendor_code}/analytics", response_class=HTMLResponse, include_in_schema=False
+)
+async def vendor_analytics_page(
+ request: Request,
+ vendor_code: str = Path(..., description="Vendor code"),
+ current_user: User = Depends(get_current_vendor_from_cookie_or_header),
+):
+ """
+ Render analytics and reports page.
+ JavaScript loads analytics data via API.
+ """
+ return templates.TemplateResponse(
+ "vendor/analytics.html",
+ {
+ "request": request,
+ "user": current_user,
+ "vendor_code": vendor_code,
+ },
+ )
+
+
# ============================================================================
# CONTENT PAGES MANAGEMENT
# ============================================================================
diff --git a/app/templates/vendor/analytics.html b/app/templates/vendor/analytics.html
new file mode 100644
index 00000000..7f261389
--- /dev/null
+++ b/app/templates/vendor/analytics.html
@@ -0,0 +1,231 @@
+{# app/templates/vendor/analytics.html #}
+{% extends "vendor/base.html" %}
+{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %}
+{% from 'shared/macros/alerts.html' import loading_state, error_state %}
+
+{% block title %}Analytics{% endblock %}
+
+{% block alpine_data %}vendorAnalytics(){% endblock %}
+
+{% block content %}
+
+{% call page_header_flex(title='Analytics', subtitle='Track your business performance') %}
+
+
+
+ {{ refresh_button(loading_var='loading', onclick='loadAllData()', variant='secondary') }}
+
+{% endcall %}
+
+{{ loading_state('Loading analytics...') }}
+
+{{ error_state('Error loading analytics') }}
+
+
+
+
+
+
+
+
+
+
+
+
Total Products
+
0
+
+ active
+ (%)
+
+
+
+
+
+
+
+
+
+
+
Total Orders
+
0
+
+ pending
+
+
+
+
+
+
+
+
+
+
+
Customers
+
0
+
Total registered
+
+
+
+
+
+
+
+
+
+
Inventory
+
0
+
+ low stock items
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Products Added
+
+
+
+
+
+
+
+
+
Locations
+ Total
+
+
+
+
+
+
+
+
inventory locations
+
+
+
+
+
+
+
+
+
+
+
+
+
Advanced Analytics
+
+ Upgrade your plan to access detailed analytics including import trends, product performance, and more.
+
+
+
+ View Plans
+
+
+
+
+
+
+
+
Quick Summary
+
+
+
+
+
+
Active Product Rate
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block extra_scripts %}
+
+{% endblock %}
diff --git a/app/templates/vendor/notifications.html b/app/templates/vendor/notifications.html
new file mode 100644
index 00000000..ef6abfe0
--- /dev/null
+++ b/app/templates/vendor/notifications.html
@@ -0,0 +1,237 @@
+{# app/templates/vendor/notifications.html #}
+{% extends "vendor/base.html" %}
+{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %}
+{% from 'shared/macros/alerts.html' import loading_state, error_state %}
+{% from 'shared/macros/pagination.html' import pagination_simple %}
+
+{% block title %}Notifications{% endblock %}
+
+{% block alpine_data %}vendorNotifications(){% endblock %}
+
+{% block content %}
+
+{% call page_header_flex(title='Notifications', subtitle='Stay updated on your store activity') %}
+
+ {{ refresh_button(loading_var='loadingNotifications', onclick='loadNotifications()', variant='secondary') }}
+
+
+{% endcall %}
+
+{{ loading_state('Loading notifications...') }}
+
+{{ error_state('Error loading notifications') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
Quick Actions
+
+
+ All caught up!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading notifications...
+
+
+
+
+
+
+
+
No notifications
+
You're all caught up!
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ View
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Showing - of
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Notification Settings
+
+
+
+
+
+
+
Email Notifications
+
Receive notifications via email
+
+
+
+
+
+
+
+
In-App Notifications
+
Show notifications in the dashboard
+
+
+
+
+
+ Note: Full notification settings management coming soon.
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block extra_scripts %}
+
+{% endblock %}
diff --git a/app/templates/vendor/partials/sidebar.html b/app/templates/vendor/partials/sidebar.html
index f52c9e80..d1d3408e 100644
--- a/app/templates/vendor/partials/sidebar.html
+++ b/app/templates/vendor/partials/sidebar.html
@@ -72,6 +72,7 @@
{{ menu_item('dashboard', 'dashboard', 'home', 'Dashboard') }}
+ {{ menu_item('analytics', 'analytics', 'chart-bar', 'Analytics') }}
@@ -95,6 +96,7 @@
{% call section_content('customers') %}
{{ menu_item('customers', 'customers', 'user-group', 'All Customers') }}
{{ menu_item('messages', 'messages', 'chat-bubble-left-right', 'Messages') }}
+ {{ menu_item('notifications', 'notifications', 'bell', 'Notifications') }}
{% endcall %}
diff --git a/docs/implementation/vendor-frontend-parity-plan.md b/docs/implementation/vendor-frontend-parity-plan.md
index 6dc160cd..8b720e5c 100644
--- a/docs/implementation/vendor-frontend-parity-plan.md
+++ b/docs/implementation/vendor-frontend-parity-plan.md
@@ -5,7 +5,7 @@
## Executive Summary
-The vendor frontend is now approximately 90% complete compared to admin. Phase 1 (Sidebar Refactor) and Phase 2 (Core JS Files) are complete. Only Phase 3 (New Features like notifications and analytics) remains.
+The vendor frontend is now approximately 95% complete compared to admin. Phase 1 (Sidebar Refactor), Phase 2 (Core JS Files), and Phase 3 (Notifications + Analytics) are complete. Only bulk operations remain as optional enhancements.
---
@@ -21,6 +21,7 @@ The vendor frontend is now approximately 90% complete compared to admin. Phase 1
### New Sidebar Structure
```
Dashboard
+Analytics
├── Products & Inventory (collapsible)
│ ├── All Products
│ ├── Inventory
@@ -31,7 +32,8 @@ Dashboard
│ └── Invoices
├── Customers & Communication (collapsible)
│ ├── Customers
-│ └── Messages
+│ ├── Messages
+│ └── Notifications
├── Shop & Content (collapsible)
│ └── Content Pages
└── Account & Settings (collapsible)
@@ -97,8 +99,8 @@ Dashboard
| Profile | - | ✅ | Complete |
| Settings | ✅ | ✅ | Complete |
| Content Pages | ✅ | ✅ | Complete |
-| Notifications | ✅ | ❌ | Missing page + JS |
-| Analytics | ✅ | ❌ | Missing page |
+| Notifications | ✅ | ✅ | Complete |
+| Analytics | ✅ | ✅ | Complete |
---
@@ -106,8 +108,8 @@ Dashboard
| Type | Admin | Vendor | Target |
|------|-------|--------|--------|
-| Total JS Files | 52 | 18 | 20+ |
-| Page Coverage | ~90% | ~90% | 90%+ |
+| Total JS Files | 52 | 20 | 20+ |
+| Page Coverage | ~90% | ~95% | 90%+ |
---
@@ -142,7 +144,7 @@ Dashboard
- [x] settings.js
- [x] content-pages.js (already exists)
-### Phase 3: New Features
-- [ ] Notifications center
-- [ ] Analytics page
-- [ ] Bulk operations
+### Phase 3: New Features ✅
+- [x] Notifications center
+- [x] Analytics page
+- [ ] Bulk operations (optional enhancement)
diff --git a/static/vendor/js/analytics.js b/static/vendor/js/analytics.js
new file mode 100644
index 00000000..b94fe3c9
--- /dev/null
+++ b/static/vendor/js/analytics.js
@@ -0,0 +1,199 @@
+// static/vendor/js/analytics.js
+/**
+ * Vendor analytics and reports page logic
+ * View business metrics and performance data
+ */
+
+const vendorAnalyticsLog = window.LogConfig.loggers.vendorAnalytics ||
+ window.LogConfig.createLogger('vendorAnalytics', false);
+
+vendorAnalyticsLog.info('Loading...');
+
+function vendorAnalytics() {
+ vendorAnalyticsLog.info('vendorAnalytics() called');
+
+ return {
+ // Inherit base layout state
+ ...data(),
+
+ // Set page identifier
+ currentPage: 'analytics',
+
+ // Loading states
+ loading: true,
+ error: '',
+
+ // Time period
+ period: '30d',
+ periodOptions: [
+ { value: '7d', label: 'Last 7 Days' },
+ { value: '30d', label: 'Last 30 Days' },
+ { value: '90d', label: 'Last 90 Days' },
+ { value: '1y', label: 'Last Year' }
+ ],
+
+ // Analytics data
+ analytics: null,
+ stats: null,
+
+ // Dashboard stats (from vendor stats endpoint)
+ dashboardStats: {
+ total_products: 0,
+ active_products: 0,
+ featured_products: 0,
+ total_orders: 0,
+ pending_orders: 0,
+ total_customers: 0,
+ total_inventory: 0,
+ low_stock_count: 0
+ },
+
+ async init() {
+ vendorAnalyticsLog.info('Analytics init() called');
+
+ // Guard against multiple initialization
+ if (window._vendorAnalyticsInitialized) {
+ vendorAnalyticsLog.warn('Already initialized, skipping');
+ return;
+ }
+ window._vendorAnalyticsInitialized = true;
+
+ try {
+ await this.loadAllData();
+ } catch (error) {
+ vendorAnalyticsLog.error('Init failed:', error);
+ this.error = 'Failed to initialize analytics page';
+ } finally {
+ this.loading = false;
+ }
+
+ vendorAnalyticsLog.info('Analytics initialization complete');
+ },
+
+ /**
+ * Load all analytics data
+ */
+ async loadAllData() {
+ this.loading = true;
+ this.error = '';
+
+ try {
+ // Load analytics and stats in parallel
+ const [analyticsResponse, statsResponse] = await Promise.all([
+ this.fetchAnalytics(),
+ this.fetchStats()
+ ]);
+
+ this.analytics = analyticsResponse;
+ this.dashboardStats = statsResponse;
+
+ vendorAnalyticsLog.info('Loaded analytics data');
+ } catch (error) {
+ vendorAnalyticsLog.error('Failed to load data:', error);
+ this.error = error.message || 'Failed to load analytics data';
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ /**
+ * Fetch analytics data for current period
+ */
+ async fetchAnalytics() {
+ try {
+ const response = await apiClient.get(`/vendor/${this.vendorCode}/analytics?period=${this.period}`);
+ return response;
+ } catch (error) {
+ // Analytics might require feature access
+ if (error.status === 403) {
+ vendorAnalyticsLog.warn('Analytics feature not available');
+ return null;
+ }
+ throw error;
+ }
+ },
+
+ /**
+ * Fetch dashboard stats
+ */
+ async fetchStats() {
+ try {
+ const response = await apiClient.get(`/vendor/${this.vendorCode}/dashboard/stats`);
+ return {
+ total_products: response.catalog?.total_products || 0,
+ active_products: response.catalog?.active_products || 0,
+ featured_products: response.catalog?.featured_products || 0,
+ total_orders: response.orders?.total || 0,
+ pending_orders: response.orders?.pending || 0,
+ total_customers: response.customers?.total || 0,
+ total_inventory: response.inventory?.total_quantity || 0,
+ low_stock_count: response.inventory?.low_stock_count || 0
+ };
+ } catch (error) {
+ vendorAnalyticsLog.error('Failed to fetch stats:', error);
+ return this.dashboardStats;
+ }
+ },
+
+ /**
+ * Change time period and reload data
+ */
+ async changePeriod(newPeriod) {
+ this.period = newPeriod;
+ await this.loadAllData();
+ },
+
+ /**
+ * Get period label
+ */
+ getPeriodLabel() {
+ const option = this.periodOptions.find(p => p.value === this.period);
+ return option ? option.label : this.period;
+ },
+
+ /**
+ * Format number with commas
+ */
+ formatNumber(num) {
+ if (num === null || num === undefined) return '0';
+ return num.toLocaleString();
+ },
+
+ /**
+ * Format percentage
+ */
+ formatPercent(value) {
+ if (value === null || value === undefined) return '0%';
+ return `${value.toFixed(1)}%`;
+ },
+
+ /**
+ * Calculate active product percentage
+ */
+ get activeProductPercent() {
+ if (!this.dashboardStats.total_products) return 0;
+ return (this.dashboardStats.active_products / this.dashboardStats.total_products * 100).toFixed(1);
+ },
+
+ /**
+ * Calculate pending order percentage
+ */
+ get pendingOrderPercent() {
+ if (!this.dashboardStats.total_orders) return 0;
+ return (this.dashboardStats.pending_orders / this.dashboardStats.total_orders * 100).toFixed(1);
+ },
+
+ /**
+ * Get stock health status
+ */
+ get stockHealth() {
+ if (this.dashboardStats.low_stock_count === 0) {
+ return { status: 'good', label: 'Healthy', color: 'green' };
+ } else if (this.dashboardStats.low_stock_count <= 5) {
+ return { status: 'warning', label: 'Attention Needed', color: 'yellow' };
+ } else {
+ return { status: 'critical', label: 'Critical', color: 'red' };
+ }
+ }
+ };
+}
diff --git a/static/vendor/js/notifications.js b/static/vendor/js/notifications.js
new file mode 100644
index 00000000..2f6ad223
--- /dev/null
+++ b/static/vendor/js/notifications.js
@@ -0,0 +1,274 @@
+// static/vendor/js/notifications.js
+/**
+ * Vendor notifications center page logic
+ * View and manage notifications
+ */
+
+const vendorNotificationsLog = window.LogConfig.loggers.vendorNotifications ||
+ window.LogConfig.createLogger('vendorNotifications', false);
+
+vendorNotificationsLog.info('Loading...');
+
+function vendorNotifications() {
+ vendorNotificationsLog.info('vendorNotifications() called');
+
+ return {
+ // Inherit base layout state
+ ...data(),
+
+ // Set page identifier
+ currentPage: 'notifications',
+
+ // Loading states
+ loading: true,
+ error: '',
+ loadingNotifications: false,
+
+ // Notifications data
+ notifications: [],
+ stats: {
+ total: 0,
+ unread_count: 0
+ },
+
+ // Pagination
+ page: 1,
+ limit: 20,
+ skip: 0,
+
+ // Filters
+ filters: {
+ priority: '',
+ is_read: ''
+ },
+
+ // Settings
+ settings: null,
+ showSettingsModal: false,
+ settingsForm: {
+ email_notifications: true,
+ in_app_notifications: true
+ },
+
+ async init() {
+ vendorNotificationsLog.info('Notifications init() called');
+
+ // Guard against multiple initialization
+ if (window._vendorNotificationsInitialized) {
+ vendorNotificationsLog.warn('Already initialized, skipping');
+ return;
+ }
+ window._vendorNotificationsInitialized = true;
+
+ try {
+ await this.loadNotifications();
+ } catch (error) {
+ vendorNotificationsLog.error('Init failed:', error);
+ this.error = 'Failed to initialize notifications page';
+ } finally {
+ this.loading = false;
+ }
+
+ vendorNotificationsLog.info('Notifications initialization complete');
+ },
+
+ /**
+ * Load notifications with current filters
+ */
+ async loadNotifications() {
+ this.loadingNotifications = true;
+ this.error = '';
+
+ try {
+ this.skip = (this.page - 1) * this.limit;
+ const params = new URLSearchParams();
+ params.append('skip', this.skip);
+ params.append('limit', this.limit);
+
+ if (this.filters.is_read === 'false') {
+ params.append('unread_only', 'true');
+ }
+
+ const response = await apiClient.get(`/vendor/${this.vendorCode}/notifications?${params}`);
+
+ this.notifications = response.notifications || [];
+ this.stats.total = response.total || 0;
+ this.stats.unread_count = response.unread_count || 0;
+
+ vendorNotificationsLog.info(`Loaded ${this.notifications.length} notifications`);
+ } catch (error) {
+ vendorNotificationsLog.error('Failed to load notifications:', error);
+ this.error = error.message || 'Failed to load notifications';
+ } finally {
+ this.loadingNotifications = false;
+ }
+ },
+
+ /**
+ * Mark notification as read
+ */
+ async markAsRead(notification) {
+ try {
+ await apiClient.put(`/vendor/${this.vendorCode}/notifications/${notification.id}/read`);
+
+ // Update local state
+ notification.is_read = true;
+ this.stats.unread_count = Math.max(0, this.stats.unread_count - 1);
+
+ Utils.showToast('Notification marked as read', 'success');
+ } catch (error) {
+ vendorNotificationsLog.error('Failed to mark as read:', error);
+ Utils.showToast(error.message || 'Failed to mark notification as read', 'error');
+ }
+ },
+
+ /**
+ * Mark all notifications as read
+ */
+ async markAllAsRead() {
+ try {
+ await apiClient.put(`/vendor/${this.vendorCode}/notifications/mark-all-read`);
+
+ // Update local state
+ this.notifications.forEach(n => n.is_read = true);
+ this.stats.unread_count = 0;
+
+ Utils.showToast('All notifications marked as read', 'success');
+ } catch (error) {
+ vendorNotificationsLog.error('Failed to mark all as read:', error);
+ Utils.showToast(error.message || 'Failed to mark all as read', 'error');
+ }
+ },
+
+ /**
+ * Delete notification
+ */
+ async deleteNotification(notificationId) {
+ if (!confirm('Are you sure you want to delete this notification?')) {
+ return;
+ }
+
+ try {
+ await apiClient.delete(`/vendor/${this.vendorCode}/notifications/${notificationId}`);
+
+ // Remove from local state
+ const wasUnread = this.notifications.find(n => n.id === notificationId && !n.is_read);
+ this.notifications = this.notifications.filter(n => n.id !== notificationId);
+ this.stats.total = Math.max(0, this.stats.total - 1);
+ if (wasUnread) {
+ this.stats.unread_count = Math.max(0, this.stats.unread_count - 1);
+ }
+
+ Utils.showToast('Notification deleted', 'success');
+ } catch (error) {
+ vendorNotificationsLog.error('Failed to delete notification:', error);
+ Utils.showToast(error.message || 'Failed to delete notification', 'error');
+ }
+ },
+
+ /**
+ * Open settings modal
+ */
+ async openSettingsModal() {
+ try {
+ const response = await apiClient.get(`/vendor/${this.vendorCode}/notifications/settings`);
+ this.settingsForm = {
+ email_notifications: response.email_notifications !== false,
+ in_app_notifications: response.in_app_notifications !== false
+ };
+ this.showSettingsModal = true;
+ } catch (error) {
+ vendorNotificationsLog.error('Failed to load settings:', error);
+ Utils.showToast(error.message || 'Failed to load notification settings', 'error');
+ }
+ },
+
+ /**
+ * Save notification settings
+ */
+ async saveSettings() {
+ try {
+ await apiClient.put(`/vendor/${this.vendorCode}/notifications/settings`, this.settingsForm);
+ Utils.showToast('Notification settings saved', 'success');
+ this.showSettingsModal = false;
+ } catch (error) {
+ vendorNotificationsLog.error('Failed to save settings:', error);
+ Utils.showToast(error.message || 'Failed to save settings', 'error');
+ }
+ },
+
+ /**
+ * Get notification icon based on type
+ */
+ getNotificationIcon(type) {
+ const icons = {
+ 'order_received': 'shopping-cart',
+ 'order_shipped': 'truck',
+ 'order_delivered': 'check-circle',
+ 'low_stock': 'exclamation-triangle',
+ 'import_complete': 'cloud-download',
+ 'import_failed': 'x-circle',
+ 'team_invite': 'user-plus',
+ 'payment_received': 'credit-card',
+ 'system': 'cog'
+ };
+ return icons[type] || 'bell';
+ },
+
+ /**
+ * Get priority color class
+ */
+ getPriorityClass(priority) {
+ const classes = {
+ 'critical': 'bg-red-100 text-red-600 dark:bg-red-900 dark:text-red-300',
+ 'high': 'bg-orange-100 text-orange-600 dark:bg-orange-900 dark:text-orange-300',
+ 'normal': 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-300',
+ 'low': 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300'
+ };
+ return classes[priority] || classes['normal'];
+ },
+
+ /**
+ * Format date for display
+ */
+ formatDate(dateString) {
+ if (!dateString) return '-';
+ const date = new Date(dateString);
+ const now = new Date();
+ const diff = Math.floor((now - date) / 1000);
+
+ // Show relative time for recent dates
+ if (diff < 60) return 'Just now';
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
+ if (diff < 172800) return 'Yesterday';
+
+ // Show full date for older dates
+ return date.toLocaleDateString();
+ },
+
+ // Pagination methods
+ get totalPages() {
+ return Math.ceil(this.stats.total / this.limit);
+ },
+
+ prevPage() {
+ if (this.page > 1) {
+ this.page--;
+ this.loadNotifications();
+ }
+ },
+
+ nextPage() {
+ if (this.page < this.totalPages) {
+ this.page++;
+ this.loadNotifications();
+ }
+ },
+
+ goToPage(pageNum) {
+ this.page = pageNum;
+ this.loadNotifications();
+ }
+ };
+}