Rename all "shop" directories and references to "storefront" to match the API and route naming convention already in use. Renamed directories: - app/templates/shop/ → app/templates/storefront/ - static/shop/ → static/storefront/ - app/templates/shared/macros/shop/ → .../macros/storefront/ - docs/frontend/shop/ → docs/frontend/storefront/ Renamed files: - shop.css → storefront.css - shop-layout.js → storefront-layout.js Updated references in: - app/routes/storefront_pages.py (21 template references) - app/modules/cms/routes/pages/vendor.py - app/templates/storefront/base.html (static paths) - All storefront templates (extends/includes) - docs/architecture/frontend-structure.md This aligns the template/static naming with: - Route file: storefront_pages.py - API directory: app/api/v1/storefront/ - Module routes: */routes/api/storefront.py - URL paths: /storefront/* Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
226 lines
11 KiB
HTML
226 lines
11 KiB
HTML
{# app/templates/storefront/account/orders.html #}
|
|
{% extends "storefront/base.html" %}
|
|
|
|
{% block title %}Order History - {{ vendor.name }}{% endblock %}
|
|
|
|
{% block alpine_data %}shopOrdersPage(){% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<!-- Breadcrumb -->
|
|
<nav class="mb-6" aria-label="Breadcrumb">
|
|
<ol class="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
|
|
<li>
|
|
<a href="{{ base_url }}shop/account/dashboard" class="hover:text-primary">My Account</a>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="h-4 w-4 mx-2" x-html="$icon('chevron-right', 'h-4 w-4')"></span>
|
|
<span class="text-gray-900 dark:text-white">Order History</span>
|
|
</li>
|
|
</ol>
|
|
</nav>
|
|
|
|
<!-- Page Header -->
|
|
<div class="mb-8">
|
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Order History</h1>
|
|
<p class="mt-2 text-gray-600 dark:text-gray-400">View and track your orders</p>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div x-show="loading" class="flex justify-center items-center py-12">
|
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary" style="border-color: var(--color-primary)"></div>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div x-show="error && !loading" class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6">
|
|
<div class="flex">
|
|
<span class="h-5 w-5 text-red-400" x-html="$icon('x-circle', 'h-5 w-5')"></span>
|
|
<p class="ml-3 text-sm text-red-700 dark:text-red-300" x-text="error"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div x-show="!loading && !error && orders.length === 0"
|
|
class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-12 text-center">
|
|
<span class="mx-auto h-12 w-12 text-gray-400 block" x-html="$icon('shopping-bag', 'h-12 w-12 mx-auto')"></span>
|
|
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">No orders yet</h3>
|
|
<p class="mt-2 text-gray-500 dark:text-gray-400">Start shopping to see your orders here.</p>
|
|
<a href="{{ base_url }}shop/products"
|
|
class="mt-6 inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary hover:bg-primary-dark transition-colors"
|
|
style="background-color: var(--color-primary)">
|
|
Browse Products
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Orders List -->
|
|
<div x-show="!loading && !error && orders.length > 0" class="space-y-4">
|
|
<template x-for="order in orders" :key="order.id">
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 overflow-hidden">
|
|
<!-- Order Header -->
|
|
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex flex-wrap items-center justify-between gap-4">
|
|
<div class="flex flex-wrap items-center gap-6">
|
|
<div>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">Order Number</p>
|
|
<p class="text-sm font-medium text-gray-900 dark:text-white" x-text="order.order_number"></p>
|
|
</div>
|
|
<div>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">Date</p>
|
|
<p class="text-sm font-medium text-gray-900 dark:text-white" x-text="formatDate(order.order_date || order.created_at)"></p>
|
|
</div>
|
|
<div>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">Total</p>
|
|
<p class="text-sm font-bold text-gray-900 dark:text-white" x-text="formatPrice(order.total_amount)"></p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-4">
|
|
<!-- Status Badge -->
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
|
:class="getStatusClass(order.status)">
|
|
<span x-text="getStatusLabel(order.status)"></span>
|
|
</span>
|
|
<!-- View Details Button -->
|
|
<a :href="'{{ base_url }}shop/account/orders/' + order.id"
|
|
class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors">
|
|
View Details
|
|
<span class="ml-2 h-4 w-4" x-html="$icon('chevron-right', 'h-4 w-4')"></span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Order Items Preview -->
|
|
<div class="px-6 py-4">
|
|
<template x-for="item in order.items.slice(0, 3)" :key="item.id">
|
|
<div class="flex items-center py-2 border-b border-gray-100 dark:border-gray-700 last:border-0">
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-sm font-medium text-gray-900 dark:text-white truncate" x-text="item.product_name"></p>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
Qty: <span x-text="item.quantity"></span> ×
|
|
<span x-text="formatPrice(item.unit_price)"></span>
|
|
</p>
|
|
</div>
|
|
<p class="ml-4 text-sm font-medium text-gray-900 dark:text-white" x-text="formatPrice(item.total_price)"></p>
|
|
</div>
|
|
</template>
|
|
<p x-show="order.items.length > 3"
|
|
class="text-sm text-gray-500 dark:text-gray-400 mt-2"
|
|
x-text="'+ ' + (order.items.length - 3) + ' more item(s)'"></p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Pagination -->
|
|
<div x-show="totalPages > 1" class="flex justify-center mt-8">
|
|
<nav class="flex items-center space-x-2">
|
|
<button @click="loadOrders(currentPage - 1)"
|
|
:disabled="currentPage === 1"
|
|
class="px-3 py-2 rounded-md border border-gray-300 dark:border-gray-600 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed">
|
|
Previous
|
|
</button>
|
|
<span class="text-sm text-gray-700 dark:text-gray-300">
|
|
Page <span x-text="currentPage"></span> of <span x-text="totalPages"></span>
|
|
</span>
|
|
<button @click="loadOrders(currentPage + 1)"
|
|
:disabled="currentPage === totalPages"
|
|
class="px-3 py-2 rounded-md border border-gray-300 dark:border-gray-600 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed">
|
|
Next
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
function shopOrdersPage() {
|
|
return {
|
|
...shopLayoutData(),
|
|
|
|
// State
|
|
orders: [],
|
|
loading: true,
|
|
error: '',
|
|
currentPage: 1,
|
|
totalPages: 1,
|
|
perPage: 10,
|
|
|
|
// Status mapping
|
|
statuses: {
|
|
pending: { label: 'Pending', class: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' },
|
|
processing: { label: 'Processing', class: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' },
|
|
partially_shipped: { label: 'Partially Shipped', class: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200' },
|
|
shipped: { label: 'Shipped', class: 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200' },
|
|
delivered: { label: 'Delivered', class: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' },
|
|
completed: { label: 'Completed', class: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' },
|
|
cancelled: { label: 'Cancelled', class: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' },
|
|
refunded: { label: 'Refunded', class: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200' }
|
|
},
|
|
|
|
async init() {
|
|
await this.loadOrders(1);
|
|
},
|
|
|
|
async loadOrders(page = 1) {
|
|
this.loading = true;
|
|
this.error = '';
|
|
|
|
try {
|
|
const token = localStorage.getItem('customer_token');
|
|
if (!token) {
|
|
window.location.href = '{{ base_url }}shop/account/login?next=' + encodeURIComponent(window.location.pathname);
|
|
return;
|
|
}
|
|
|
|
const skip = (page - 1) * this.perPage;
|
|
const response = await fetch(`/api/v1/shop/orders?skip=${skip}&limit=${this.perPage}`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
localStorage.removeItem('customer_token');
|
|
localStorage.removeItem('customer_user');
|
|
window.location.href = '{{ base_url }}shop/account/login?next=' + encodeURIComponent(window.location.pathname);
|
|
return;
|
|
}
|
|
throw new Error('Failed to load orders');
|
|
}
|
|
|
|
const data = await response.json();
|
|
this.orders = data.orders || [];
|
|
this.currentPage = page;
|
|
this.totalPages = Math.ceil((data.total || 0) / this.perPage);
|
|
|
|
} catch (err) {
|
|
console.error('Error loading orders:', err);
|
|
this.error = err.message || 'Failed to load orders';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
getStatusLabel(status) {
|
|
return this.statuses[status]?.label || status;
|
|
},
|
|
|
|
getStatusClass(status) {
|
|
return this.statuses[status]?.class || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200';
|
|
},
|
|
|
|
// formatPrice is inherited from shopLayoutData() via spread operator
|
|
|
|
formatDate(dateStr) {
|
|
if (!dateStr) return '-';
|
|
return new Date(dateStr).toLocaleDateString('de-DE', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
});
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|