Files
orion/app/modules/analytics/templates/analytics/store/analytics.html
Samir Boulahtit adc36246b8
Some checks failed
CI / ruff (push) Successful in 14s
CI / pytest (push) Failing after 2h32m45s
CI / validate (push) Successful in 30s
CI / dependency-scanning (push) Successful in 31s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
feat(storefront): homepage, module gating, widget protocol, i18n fixes
Storefront homepage & module gating:
- CMS owns storefront GET / (slug="home" with 3-tier resolution)
- Catalog loses GET / (keeps /products only)
- Store root redirect (GET / → /store/dashboard or /store/login)
- Route gating: non-core modules return 404 when disabled for platform
- Seed store default homepages per platform

Widget protocol for customer dashboard:
- StorefrontDashboardCard contract in widgets.py
- Widget aggregator get_storefront_dashboard_cards()
- Orders and Loyalty module widget providers
- Dashboard template renders contributed cards (no module names)

Landing template module-agnostic:
- CTAs driven by storefront_nav (not hardcoded module names)
- Header actions check nav item IDs (not enabled_modules)
- Remove hardcoded "Add Product" sidebar button
- Remove all enabled_modules checks from storefront templates

i18n fixes:
- Title placeholder resolution ({{store_name}}) for store default pages
- Storefront nav label_keys prefixed with module code
- Add storefront.account.* keys to 6 modules (en/fr/de/lb)
- Header/footer CMS pages use get_translated_title(current_language)
- Footer labels use i18n keys instead of hardcoded English

Icon cleanup:
- Standardize on map-pin (remove location-marker alias)
- Replace all location-marker references across templates and docs

Docs:
- Storefront builder vision proposal (6 phases)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:53:17 +02:00

232 lines
12 KiB
HTML

{# app/modules/analytics/templates/analytics/store/analytics.html #}
{% extends "store/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 %}storeAnalytics(){% 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('map-pin', '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', '/store/' + storeCode + '/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 defer src="{{ url_for('analytics_static', path='store/js/analytics.js') }}"></script>
{% endblock %}