feat: add vendor notifications and analytics pages (Phase 3)

New pages:
- Notifications center: view, mark read, delete, settings modal
- Analytics: period selector, stats overview, feature-gated advanced metrics

Changes:
- Add routes for /vendor/{code}/notifications and /analytics
- Create notifications.js and analytics.js with full functionality
- Create notifications.html and analytics.html templates
- Update sidebar with Analytics link and Notifications in Customers section
- Update vendor-frontend-parity-plan.md to mark Phase 3 complete

Vendor frontend now at ~95% parity with admin.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-01 16:35:52 +01:00
parent 463e1b67d5
commit 646d789af7
7 changed files with 1009 additions and 10 deletions

View File

@@ -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
# ============================================================================

231
app/templates/vendor/analytics.html vendored Normal file
View File

@@ -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 %}
<!-- Page Header -->
{% call page_header_flex(title='Analytics', subtitle='Track your business performance') %}
<div class="flex items-center gap-4">
<!-- Period Selector -->
<select
x-model="period"
@change="changePeriod(period)"
class="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300"
>
<template x-for="option in periodOptions" :key="option.value">
<option :value="option.value" x-text="option.label"></option>
</template>
</select>
{{ refresh_button(loading_var='loading', onclick='loadAllData()', variant='secondary') }}
</div>
{% endcall %}
{{ loading_state('Loading analytics...') }}
{{ error_state('Error loading analytics') }}
<!-- Analytics Content -->
<div x-show="!loading && !error" class="space-y-8">
<!-- Overview Stats -->
<div class="grid gap-6 md:grid-cols-2 xl:grid-cols-4">
<!-- Products -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-purple-500 bg-purple-100 rounded-full dark:text-purple-100 dark:bg-purple-500">
<span x-html="$icon('cube', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Total Products</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(dashboardStats.total_products)">0</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
<span x-text="formatNumber(dashboardStats.active_products)"></span> active
(<span x-text="activeProductPercent"></span>%)
</p>
</div>
</div>
<!-- Orders -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
<span x-html="$icon('shopping-cart', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Total Orders</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(dashboardStats.total_orders)">0</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
<span x-text="formatNumber(dashboardStats.pending_orders)"></span> pending
</p>
</div>
</div>
<!-- Customers -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-100 dark:bg-green-500">
<span x-html="$icon('users', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Customers</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(dashboardStats.total_customers)">0</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Total registered</p>
</div>
</div>
<!-- Inventory -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 rounded-full"
:class="{
'text-green-500 bg-green-100 dark:text-green-100 dark:bg-green-500': stockHealth.color === 'green',
'text-yellow-500 bg-yellow-100 dark:text-yellow-100 dark:bg-yellow-500': stockHealth.color === 'yellow',
'text-red-500 bg-red-100 dark:text-red-100 dark:bg-red-500': stockHealth.color === 'red'
}">
<span x-html="$icon('archive', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Inventory</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(dashboardStats.total_inventory)">0</p>
<p class="text-xs" :class="{
'text-green-600 dark:text-green-400': stockHealth.color === 'green',
'text-yellow-600 dark:text-yellow-400': stockHealth.color === 'yellow',
'text-red-600 dark:text-red-400': stockHealth.color === 'red'
}">
<span x-text="dashboardStats.low_stock_count"></span> low stock items
</p>
</div>
</div>
</div>
<!-- Period Analytics (if available) -->
<template x-if="analytics">
<div class="grid gap-6 md:grid-cols-3">
<!-- Imports This Period -->
<div class="p-6 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Imports</h3>
<span class="text-sm text-gray-500 dark:text-gray-400" x-text="getPeriodLabel()"></span>
</div>
<div class="flex items-center">
<div class="p-3 mr-4 text-teal-500 bg-teal-100 rounded-full dark:text-teal-100 dark:bg-teal-500">
<span x-html="$icon('cloud-download', 'w-6 h-6')"></span>
</div>
<div>
<p class="text-2xl font-bold text-gray-700 dark:text-gray-200" x-text="formatNumber(analytics.imports?.count || 0)"></p>
<p class="text-sm text-gray-500 dark:text-gray-400">import jobs</p>
</div>
</div>
</div>
<!-- Products Added This Period -->
<div class="p-6 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Products Added</h3>
<span class="text-sm text-gray-500 dark:text-gray-400" x-text="getPeriodLabel()"></span>
</div>
<div class="flex items-center">
<div class="p-3 mr-4 text-indigo-500 bg-indigo-100 rounded-full dark:text-indigo-100 dark:bg-indigo-500">
<span x-html="$icon('plus-circle', 'w-6 h-6')"></span>
</div>
<div>
<p class="text-2xl font-bold text-gray-700 dark:text-gray-200" x-text="formatNumber(analytics.catalog?.products_added || 0)"></p>
<p class="text-sm text-gray-500 dark:text-gray-400">new products</p>
</div>
</div>
</div>
<!-- Inventory Locations -->
<div class="p-6 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Locations</h3>
<span class="text-sm text-gray-500 dark:text-gray-400">Total</span>
</div>
<div class="flex items-center">
<div class="p-3 mr-4 text-orange-500 bg-orange-100 rounded-full dark:text-orange-100 dark:bg-orange-500">
<span x-html="$icon('location-marker', 'w-6 h-6')"></span>
</div>
<div>
<p class="text-2xl font-bold text-gray-700 dark:text-gray-200" x-text="formatNumber(analytics.inventory?.total_locations || 0)"></p>
<p class="text-sm text-gray-500 dark:text-gray-400">inventory locations</p>
</div>
</div>
</div>
</div>
</template>
<!-- Feature Upgrade Prompt (if analytics not available) -->
<template x-if="!analytics">
<div class="p-8 bg-white rounded-lg shadow-xs dark:bg-gray-800 text-center">
<div class="mx-auto w-16 h-16 mb-4 flex items-center justify-center bg-purple-100 dark:bg-purple-900 rounded-full">
<span x-html="$icon('chart-bar', 'w-8 h-8 text-purple-600 dark:text-purple-400')"></span>
</div>
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-2">Advanced Analytics</h3>
<p class="text-gray-500 dark:text-gray-400 mb-4">
Upgrade your plan to access detailed analytics including import trends, product performance, and more.
</p>
<a href="#" @click.prevent="$dispatch('navigate', '/vendor/' + vendorCode + '/billing')"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700">
<span x-html="$icon('sparkles', 'w-4 h-4 mr-2')"></span>
View Plans
</a>
</div>
</template>
<!-- Quick Stats Summary -->
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800 overflow-hidden">
<div class="p-4 border-b dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Quick Summary</h3>
</div>
<div class="p-4">
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<!-- Active Products Rate -->
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">Active Product Rate</p>
<div class="flex items-center">
<p class="text-xl font-bold text-gray-700 dark:text-gray-200" x-text="activeProductPercent + '%'"></p>
<span class="ml-2 text-green-500" x-show="activeProductPercent >= 80">
<span x-html="$icon('trending-up', 'w-4 h-4')"></span>
</span>
</div>
</div>
<!-- Featured Products -->
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">Featured Products</p>
<p class="text-xl font-bold text-gray-700 dark:text-gray-200" x-text="formatNumber(dashboardStats.featured_products)"></p>
</div>
<!-- Pending Orders -->
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">Pending Orders</p>
<div class="flex items-center">
<p class="text-xl font-bold text-gray-700 dark:text-gray-200" x-text="formatNumber(dashboardStats.pending_orders)"></p>
<span class="ml-2 text-yellow-500" x-show="dashboardStats.pending_orders > 0">
<span x-html="$icon('clock', 'w-4 h-4')"></span>
</span>
</div>
</div>
<!-- Stock Health -->
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">Stock Health</p>
<div class="flex items-center">
<span class="px-2 py-1 text-sm font-medium rounded-full"
:class="{
'text-green-700 bg-green-100 dark:bg-green-900 dark:text-green-300': stockHealth.color === 'green',
'text-yellow-700 bg-yellow-100 dark:bg-yellow-900 dark:text-yellow-300': stockHealth.color === 'yellow',
'text-red-700 bg-red-100 dark:bg-red-900 dark:text-red-300': stockHealth.color === 'red'
}"
x-text="stockHealth.label"></span>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('static', path='vendor/js/analytics.js') }}"></script>
{% endblock %}

237
app/templates/vendor/notifications.html vendored Normal file
View File

@@ -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 %}
<!-- Page Header -->
{% call page_header_flex(title='Notifications', subtitle='Stay updated on your store activity') %}
<div class="flex items-center gap-4">
{{ refresh_button(loading_var='loadingNotifications', onclick='loadNotifications()', variant='secondary') }}
<button
@click="openSettingsModal()"
class="flex items-center px-4 py-2 text-sm font-medium text-gray-600 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 dark:hover:bg-gray-600"
>
<span x-html="$icon('cog', 'w-4 h-4 mr-2')"></span>
Settings
</button>
</div>
{% endcall %}
{{ loading_state('Loading notifications...') }}
{{ error_state('Error loading notifications') }}
<!-- Stats Cards -->
<div x-show="!loading" class="grid gap-6 mb-8 md:grid-cols-3">
<!-- Unread Notifications -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
<span x-html="$icon('bell', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Unread</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.unread_count">0</p>
</div>
</div>
<!-- Total Notifications -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-purple-500 bg-purple-100 rounded-full dark:text-purple-100 dark:bg-purple-500">
<span x-html="$icon('inbox', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Total</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.total">0</p>
</div>
</div>
<!-- Quick Actions -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="flex-1">
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Quick Actions</p>
<button
x-show="stats.unread_count > 0"
@click="markAllAsRead()"
class="text-sm text-purple-600 hover:text-purple-800 dark:text-purple-400"
>
Mark all as read
</button>
<span x-show="stats.unread_count === 0" class="text-sm text-gray-500 dark:text-gray-400">
All caught up!
</span>
</div>
</div>
</div>
<!-- Filters -->
<div x-show="!loading && !error" class="flex flex-wrap items-center gap-4 px-4 py-3 mb-6 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<select
x-model="filters.is_read"
@change="page = 1; loadNotifications()"
class="px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300"
>
<option value="">All Notifications</option>
<option value="false">Unread Only</option>
<option value="true">Read Only</option>
</select>
</div>
<!-- Notifications List -->
<div x-show="!loading && !error" class="bg-white rounded-lg shadow-xs dark:bg-gray-800 overflow-hidden">
<!-- Loading state for list -->
<template x-if="loadingNotifications && notifications.length === 0">
<div class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<span x-html="$icon('spinner', 'w-6 h-6 mx-auto mb-2 animate-spin')"></span>
<p>Loading notifications...</p>
</div>
</template>
<!-- Empty state -->
<template x-if="!loadingNotifications && notifications.length === 0">
<div class="px-4 py-12 text-center text-gray-500 dark:text-gray-400">
<span x-html="$icon('bell', 'w-12 h-12 mx-auto mb-3 text-gray-300 dark:text-gray-600')"></span>
<p class="font-medium">No notifications</p>
<p class="text-sm mt-1">You're all caught up!</p>
</div>
</template>
<!-- Notifications list -->
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
<template x-for="notif in notifications" :key="notif.id">
<li class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
:class="notif.is_read ? 'opacity-60' : ''">
<div class="flex items-start px-4 py-4">
<!-- Icon -->
<div class="flex-shrink-0 mr-4">
<span class="inline-flex items-center justify-center w-10 h-10 rounded-full"
:class="getPriorityClass(notif.priority)">
<span x-html="$icon(getNotificationIcon(notif.type), 'w-5 h-5')"></span>
</span>
</div>
<!-- Content -->
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between">
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100" x-text="notif.title"></p>
<span class="text-xs text-gray-400" x-text="formatDate(notif.created_at)"></span>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1" x-text="notif.message"></p>
<div class="flex items-center gap-4 mt-2">
<span class="inline-flex items-center px-2 py-0.5 text-xs font-medium rounded capitalize"
:class="getPriorityClass(notif.priority)"
x-text="notif.priority || 'normal'"></span>
<span class="text-xs text-gray-500" x-text="(notif.type || '').replace('_', ' ')"></span>
</div>
</div>
<!-- Actions -->
<div class="flex items-center gap-2 ml-4">
<template x-if="notif.action_url">
<a :href="notif.action_url"
class="px-3 py-1 text-xs font-medium text-purple-600 bg-purple-100 rounded hover:bg-purple-200 dark:bg-purple-900 dark:text-purple-300">
View
</a>
</template>
<template x-if="!notif.is_read">
<button @click="markAsRead(notif)"
class="px-3 py-1 text-xs font-medium text-gray-600 bg-gray-100 rounded hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300">
Mark read
</button>
</template>
<button @click="deleteNotification(notif.id)"
class="p-1 text-gray-400 hover:text-red-500 transition-colors">
<span x-html="$icon('trash', 'w-4 h-4')"></span>
</button>
</div>
</div>
</li>
</template>
</ul>
<!-- Pagination -->
<div x-show="stats.total > limit" class="flex items-center justify-between px-4 py-3 border-t dark:border-gray-700">
<span class="text-sm text-gray-600 dark:text-gray-400">
Showing <span x-text="skip + 1"></span>-<span x-text="Math.min(skip + limit, stats.total)"></span> of <span x-text="stats.total"></span>
</span>
<div class="flex items-center gap-2">
<button
@click="prevPage()"
:disabled="page <= 1"
class="px-3 py-1 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50"
>
<span x-html="$icon('chevron-left', 'w-4 h-4')"></span>
</button>
<span class="text-sm text-gray-600 dark:text-gray-400" x-text="`${page} / ${totalPages}`"></span>
<button
@click="nextPage()"
:disabled="page >= totalPages"
class="px-3 py-1 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50"
>
<span x-html="$icon('chevron-right', 'w-4 h-4')"></span>
</button>
</div>
</div>
</div>
<!-- Settings Modal -->
<div x-show="showSettingsModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black bg-opacity-50">
<div class="w-full max-w-md bg-white rounded-lg shadow-xl dark:bg-gray-800" @click.away="showSettingsModal = false">
<div class="flex items-center justify-between p-4 border-b dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Notification Settings</h3>
<button @click="showSettingsModal = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
<span x-html="$icon('x', 'w-5 h-5')"></span>
</button>
</div>
<div class="p-4 space-y-4">
<!-- Email Notifications -->
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div>
<p class="font-medium text-gray-700 dark:text-gray-300">Email Notifications</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Receive notifications via email</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="settingsForm.email_notifications" class="sr-only peer" />
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-300 dark:peer-focus:ring-purple-800 rounded-full peer dark:bg-gray-600 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-500 peer-checked:bg-purple-600"></div>
</label>
</div>
<!-- In-App Notifications -->
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div>
<p class="font-medium text-gray-700 dark:text-gray-300">In-App Notifications</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Show notifications in the dashboard</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="settingsForm.in_app_notifications" class="sr-only peer" />
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-300 dark:peer-focus:ring-purple-800 rounded-full peer dark:bg-gray-600 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-500 peer-checked:bg-purple-600"></div>
</label>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400 italic">
Note: Full notification settings management coming soon.
</p>
</div>
<div class="flex justify-end gap-2 p-4 border-t dark:border-gray-700">
<button @click="showSettingsModal = false" class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200">
Cancel
</button>
<button
@click="saveSettings()"
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700"
>
Save Settings
</button>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('static', path='vendor/js/notifications.js') }}"></script>
{% endblock %}

View File

@@ -72,6 +72,7 @@
<!-- Dashboard (always visible) -->
<ul class="mt-6">
{{ menu_item('dashboard', 'dashboard', 'home', 'Dashboard') }}
{{ menu_item('analytics', 'analytics', 'chart-bar', 'Analytics') }}
</ul>
<!-- Products & Inventory Section -->
@@ -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 %}
<!-- Shop & Content Section -->