- Add shop messages API endpoints (/api/v1/shop/messages) - Add shop messages page routes (/shop/account/messages) - Add messages.html template for customer account - Add Messages card to account dashboard with unread badge - Customers can view/reply to vendor_customer conversations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
253 lines
13 KiB
HTML
253 lines
13 KiB
HTML
{# app/templates/shop/account/dashboard.html #}
|
|
{% extends "shop/base.html" %}
|
|
|
|
{% block title %}My Account - {{ vendor.name }}{% endblock %}
|
|
|
|
{% block alpine_data %}accountDashboard(){% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<!-- Page Header -->
|
|
<div class="mb-8">
|
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">My Account</h1>
|
|
<p class="mt-2 text-gray-600 dark:text-gray-400">Welcome back, {{ user.first_name }}!</p>
|
|
</div>
|
|
|
|
<!-- Dashboard Grid -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
|
|
|
<!-- Orders Card -->
|
|
<a href="{{ base_url }}shop/account/orders"
|
|
class="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-6 border border-gray-200 dark:border-gray-700">
|
|
<div class="flex items-center mb-4">
|
|
<div class="flex-shrink-0">
|
|
<svg class="h-8 w-8 text-primary" style="color: var(--color-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
|
|
</svg>
|
|
</div>
|
|
<div class="ml-4">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Orders</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">View order history</p>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<p class="text-2xl font-bold text-primary" style="color: var(--color-primary)">{{ user.total_orders }}</p>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">Total Orders</p>
|
|
</div>
|
|
</a>
|
|
|
|
<!-- Profile Card -->
|
|
<a href="{{ base_url }}shop/account/profile"
|
|
class="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-6 border border-gray-200 dark:border-gray-700">
|
|
<div class="flex items-center mb-4">
|
|
<div class="flex-shrink-0">
|
|
<svg class="h-8 w-8 text-primary" style="color: var(--color-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
</svg>
|
|
</div>
|
|
<div class="ml-4">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Profile</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">Edit your information</p>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300 truncate">{{ user.email }}</p>
|
|
</div>
|
|
</a>
|
|
|
|
<!-- Addresses Card -->
|
|
<a href="{{ base_url }}shop/account/addresses"
|
|
class="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-6 border border-gray-200 dark:border-gray-700">
|
|
<div class="flex items-center mb-4">
|
|
<div class="flex-shrink-0">
|
|
<svg class="h-8 w-8 text-primary" style="color: var(--color-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
</div>
|
|
<div class="ml-4">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Addresses</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">Manage addresses</p>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
|
|
<!-- Messages Card -->
|
|
<a href="{{ base_url }}shop/account/messages"
|
|
class="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-6 border border-gray-200 dark:border-gray-700"
|
|
x-data="{ unreadCount: 0 }"
|
|
x-init="fetch('/api/v1/shop/messages/unread-count').then(r => r.json()).then(d => unreadCount = d.unread_count).catch(() => {})">
|
|
<div class="flex items-center mb-4">
|
|
<div class="flex-shrink-0 relative">
|
|
<svg class="h-8 w-8 text-primary" style="color: var(--color-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
|
</svg>
|
|
<span x-show="unreadCount > 0"
|
|
class="absolute -top-1 -right-1 inline-flex items-center justify-center px-1.5 py-0.5 text-xs font-bold leading-none text-white bg-red-600 rounded-full"
|
|
x-text="unreadCount > 9 ? '9+' : unreadCount"></span>
|
|
</div>
|
|
<div class="ml-4">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Messages</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">Contact support</p>
|
|
</div>
|
|
</div>
|
|
<div x-show="unreadCount > 0">
|
|
<p class="text-sm text-primary font-medium" style="color: var(--color-primary)" x-text="unreadCount + ' unread message' + (unreadCount > 1 ? 's' : '')"></p>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Account Summary -->
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 border border-gray-200 dark:border-gray-700">
|
|
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">Account Summary</h3>
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">Customer Since</p>
|
|
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ user.created_at.strftime('%B %Y') }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">Total Orders</p>
|
|
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ user.total_orders }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">Customer Number</p>
|
|
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ user.customer_number }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="mt-8 flex justify-end">
|
|
<button @click="showLogoutModal = true"
|
|
class="px-6 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors">
|
|
Logout
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Logout Confirmation Modal -->
|
|
<div x-show="showLogoutModal"
|
|
x-cloak
|
|
class="fixed inset-0 z-50 overflow-y-auto"
|
|
aria-labelledby="modal-title"
|
|
role="dialog"
|
|
aria-modal="true">
|
|
<!-- Background overlay -->
|
|
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
|
<!-- Overlay backdrop -->
|
|
<div x-show="showLogoutModal"
|
|
x-transition:enter="ease-out duration-300"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
x-transition:leave="ease-in duration-200"
|
|
x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0"
|
|
@click="showLogoutModal = false"
|
|
class="fixed inset-0 bg-gray-500 dark:bg-gray-900 bg-opacity-75 dark:bg-opacity-75 transition-opacity"
|
|
aria-hidden="true">
|
|
</div>
|
|
|
|
<!-- Center modal -->
|
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
|
|
|
<!-- Modal panel -->
|
|
<div x-show="showLogoutModal"
|
|
x-transition:enter="ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
|
x-transition:leave="ease-in duration-200"
|
|
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
|
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
|
|
|
|
<div class="sm:flex sm:items-start">
|
|
<!-- Icon -->
|
|
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900 sm:mx-0 sm:h-10 sm:w-10">
|
|
<svg class="h-6 w-6 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
</svg>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
|
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white" id="modal-title">
|
|
Logout Confirmation
|
|
</h3>
|
|
<div class="mt-2">
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
Are you sure you want to logout? You'll need to sign in again to access your account.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
|
<button @click="confirmLogout()"
|
|
type="button"
|
|
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm transition-colors">
|
|
Logout
|
|
</button>
|
|
<button @click="showLogoutModal = false"
|
|
type="button"
|
|
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-700 text-base font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm transition-colors">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
function accountDashboard() {
|
|
return {
|
|
...shopLayoutData(),
|
|
showLogoutModal: false,
|
|
|
|
confirmLogout() {
|
|
// Close modal
|
|
this.showLogoutModal = false;
|
|
|
|
fetch('/api/v1/shop/auth/logout', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
})
|
|
.then(response => {
|
|
if (response.ok) {
|
|
// Clear localStorage token if any
|
|
localStorage.removeItem('customer_token');
|
|
|
|
// Show success message
|
|
this.showToast('Logged out successfully', 'success');
|
|
|
|
// Redirect to login page
|
|
setTimeout(() => {
|
|
window.location.href = '{{ base_url }}shop/account/login';
|
|
}, 500);
|
|
} else {
|
|
console.error('Logout failed with status:', response.status);
|
|
this.showToast('Logout failed', 'error');
|
|
// Still redirect on failure (cookie might be deleted)
|
|
setTimeout(() => {
|
|
window.location.href = '{{ base_url }}shop/account/login';
|
|
}, 1000);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Logout error:', error);
|
|
this.showToast('Logout failed', 'error');
|
|
// Redirect anyway
|
|
setTimeout(() => {
|
|
window.location.href = '{{ base_url }}shop/account/login';
|
|
}, 1000);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|