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:
@@ -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
231
app/templates/vendor/analytics.html
vendored
Normal 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
237
app/templates/vendor/notifications.html
vendored
Normal 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 %}
|
||||
2
app/templates/vendor/partials/sidebar.html
vendored
2
app/templates/vendor/partials/sidebar.html
vendored
@@ -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 -->
|
||||
|
||||
Reference in New Issue
Block a user