Convert 90 inline SVG icons to use the shared $icon() helper function across shop and vendor templates for consistency and maintainability. Templates updated: - Shop: checkout, products, login, register, forgot/reset-password - Shop account: addresses, dashboard, messages, order-detail, orders, profile - Vendor: billing, login, onboarding, team, landing pages (4 variants) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
560 lines
30 KiB
HTML
560 lines
30 KiB
HTML
{# app/templates/shop/account/order-detail.html #}
|
|
{% extends "shop/base.html" %}
|
|
|
|
{% block title %}Order Details - {{ vendor.name }}{% endblock %}
|
|
|
|
{% block alpine_data %}shopOrderDetailPage(){% 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>
|
|
<a href="{{ base_url }}shop/account/orders" class="hover:text-primary">Orders</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" x-text="order?.order_number || 'Order Details'"></span>
|
|
</li>
|
|
</ol>
|
|
</nav>
|
|
|
|
<!-- 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-6">
|
|
<div class="flex">
|
|
<span class="h-6 w-6 text-red-400" x-html="$icon('x-circle', 'h-6 w-6')"></span>
|
|
<div class="ml-3">
|
|
<h3 class="text-lg font-medium text-red-800 dark:text-red-200">Error loading order</h3>
|
|
<p class="mt-1 text-sm text-red-700 dark:text-red-300" x-text="error"></p>
|
|
<a href="{{ base_url }}shop/account/orders"
|
|
class="mt-4 inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700">
|
|
Back to Orders
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Order Content -->
|
|
<div x-show="!loading && !error && order" x-cloak>
|
|
<!-- Page Header -->
|
|
<div class="mb-8 flex flex-wrap items-start justify-between gap-4">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white" x-text="'Order ' + order.order_number"></h1>
|
|
<p class="mt-2 text-gray-600 dark:text-gray-400">
|
|
Placed on <span x-text="formatDateTime(order.order_date || order.created_at)"></span>
|
|
</p>
|
|
</div>
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
|
|
:class="getStatusClass(order.status)">
|
|
<span x-text="getStatusLabel(order.status)"></span>
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Order Tracking Timeline -->
|
|
<div class="mb-8 bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-6">
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-6">Order Progress</h2>
|
|
<div class="relative">
|
|
<!-- Timeline Line -->
|
|
<div class="absolute left-4 top-0 bottom-0 w-0.5 bg-gray-200 dark:bg-gray-700"></div>
|
|
|
|
<!-- Timeline Steps -->
|
|
<div class="space-y-6">
|
|
<!-- Pending -->
|
|
<div class="relative flex items-start">
|
|
<div class="flex items-center justify-center w-8 h-8 rounded-full shrink-0 z-10"
|
|
:class="getTimelineStepClass('pending')">
|
|
<span class="w-4 h-4" x-html="$icon('check-circle', 'w-4 h-4')"></span>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="font-medium text-gray-900 dark:text-white">Order Placed</p>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="formatDateTime(order.order_date || order.created_at)"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Processing -->
|
|
<div class="relative flex items-start">
|
|
<div class="flex items-center justify-center w-8 h-8 rounded-full shrink-0 z-10"
|
|
:class="getTimelineStepClass('processing')">
|
|
<span x-show="isStepComplete('processing')" class="w-4 h-4" x-html="$icon('check-circle', 'w-4 h-4')"></span>
|
|
<span x-show="!isStepComplete('processing')" class="w-4 h-4" x-html="$icon('clock', 'w-4 h-4')"></span>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="font-medium" :class="isStepComplete('processing') ? 'text-gray-900 dark:text-white' : 'text-gray-400 dark:text-gray-500'">Processing</p>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400" x-show="order.confirmed_at" x-text="formatDateTime(order.confirmed_at)"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Shipped -->
|
|
<div class="relative flex items-start">
|
|
<div class="flex items-center justify-center w-8 h-8 rounded-full shrink-0 z-10"
|
|
:class="getTimelineStepClass('shipped')">
|
|
<span x-show="isStepComplete('shipped')" class="w-4 h-4" x-html="$icon('check-circle', 'w-4 h-4')"></span>
|
|
<span x-show="!isStepComplete('shipped')" class="w-4 h-4" x-html="$icon('truck', 'w-4 h-4')"></span>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="font-medium" :class="isStepComplete('shipped') ? 'text-gray-900 dark:text-white' : 'text-gray-400 dark:text-gray-500'">Shipped</p>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400" x-show="order.shipped_at" x-text="formatDateTime(order.shipped_at)"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delivered -->
|
|
<div class="relative flex items-start">
|
|
<div class="flex items-center justify-center w-8 h-8 rounded-full shrink-0 z-10"
|
|
:class="getTimelineStepClass('delivered')">
|
|
<span x-show="isStepComplete('delivered')" class="w-4 h-4" x-html="$icon('check-circle', 'w-4 h-4')"></span>
|
|
<span x-show="!isStepComplete('delivered')" class="w-4 h-4" x-html="$icon('gift', 'w-4 h-4')"></span>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="font-medium" :class="isStepComplete('delivered') ? 'text-gray-900 dark:text-white' : 'text-gray-400 dark:text-gray-500'">Delivered</p>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400" x-show="order.delivered_at" x-text="formatDateTime(order.delivered_at)"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cancelled/Refunded Notice -->
|
|
<div x-show="order.status === 'cancelled' || order.status === 'refunded'"
|
|
class="mt-6 p-4 rounded-lg"
|
|
:class="order.status === 'cancelled' ? 'bg-red-50 dark:bg-red-900/20' : 'bg-gray-50 dark:bg-gray-700'">
|
|
<p class="text-sm font-medium"
|
|
:class="order.status === 'cancelled' ? 'text-red-800 dark:text-red-200' : 'text-gray-800 dark:text-gray-200'"
|
|
x-text="order.status === 'cancelled' ? 'This order was cancelled' : 'This order was refunded'"></p>
|
|
<p class="text-sm mt-1"
|
|
:class="order.status === 'cancelled' ? 'text-red-600 dark:text-red-300' : 'text-gray-600 dark:text-gray-400'"
|
|
x-show="order.cancelled_at"
|
|
x-text="'on ' + formatDateTime(order.cancelled_at)"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
<!-- Main Content (Left Column - 2/3) -->
|
|
<div class="lg:col-span-2 space-y-6">
|
|
|
|
<!-- Order Items -->
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700">
|
|
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Order Items</h2>
|
|
</div>
|
|
<div class="divide-y divide-gray-200 dark:divide-gray-700">
|
|
<template x-for="item in order.items" :key="item.id">
|
|
<div class="px-6 py-4 flex items-center gap-4">
|
|
<!-- Product Image Placeholder -->
|
|
<div class="flex-shrink-0 w-16 h-16 bg-gray-100 dark:bg-gray-700 rounded-lg flex items-center justify-center">
|
|
<span class="h-8 w-8 text-gray-400" x-html="$icon('cube', 'h-8 w-8')"></span>
|
|
</div>
|
|
<!-- Product Info -->
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-sm font-medium text-gray-900 dark:text-white" x-text="item.product_name"></p>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
SKU: <span x-text="item.product_sku || '-'"></span>
|
|
</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>
|
|
<!-- Line Total -->
|
|
<div class="text-right">
|
|
<p class="text-sm font-semibold text-gray-900 dark:text-white" x-text="formatPrice(item.total_price)"></p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Addresses -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<!-- Shipping Address -->
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
|
|
<span class="h-5 w-5 mr-2 text-gray-400" x-html="$icon('location-marker', 'h-5 w-5')"></span>
|
|
Shipping Address
|
|
</h3>
|
|
<div class="text-sm text-gray-600 dark:text-gray-300 space-y-1">
|
|
<p class="font-medium" x-text="(order.ship_first_name || '') + ' ' + (order.ship_last_name || '')"></p>
|
|
<p x-show="order.ship_company" x-text="order.ship_company"></p>
|
|
<p x-text="order.ship_address_line_1"></p>
|
|
<p x-show="order.ship_address_line_2" x-text="order.ship_address_line_2"></p>
|
|
<p x-text="(order.ship_postal_code || '') + ' ' + (order.ship_city || '')"></p>
|
|
<p x-text="order.ship_country_iso"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Billing Address -->
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
|
|
<span class="h-5 w-5 mr-2 text-gray-400" x-html="$icon('document-text', 'h-5 w-5')"></span>
|
|
Billing Address
|
|
</h3>
|
|
<div class="text-sm text-gray-600 dark:text-gray-300 space-y-1">
|
|
<p class="font-medium" x-text="(order.bill_first_name || '') + ' ' + (order.bill_last_name || '')"></p>
|
|
<p x-show="order.bill_company" x-text="order.bill_company"></p>
|
|
<p x-text="order.bill_address_line_1"></p>
|
|
<p x-show="order.bill_address_line_2" x-text="order.bill_address_line_2"></p>
|
|
<p x-text="(order.bill_postal_code || '') + ' ' + (order.bill_city || '')"></p>
|
|
<p x-text="order.bill_country_iso"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Customer Notes -->
|
|
<div x-show="order.customer_notes" class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
|
|
<span class="h-5 w-5 mr-2 text-gray-400" x-html="$icon('chat-bubble-left', 'h-5 w-5')"></span>
|
|
Order Notes
|
|
</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-300" x-text="order.customer_notes"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sidebar (Right Column - 1/3) -->
|
|
<div class="space-y-6">
|
|
<!-- Order Summary -->
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Order Summary</h3>
|
|
<dl class="space-y-3">
|
|
<div class="flex justify-between text-sm">
|
|
<dt class="text-gray-500 dark:text-gray-400">Subtotal</dt>
|
|
<dd class="font-medium text-gray-900 dark:text-white" x-text="formatPrice(order.subtotal)"></dd>
|
|
</div>
|
|
<div class="flex justify-between text-sm">
|
|
<dt class="text-gray-500 dark:text-gray-400">Shipping</dt>
|
|
<dd class="font-medium text-gray-900 dark:text-white" x-text="formatPrice(order.shipping_amount)"></dd>
|
|
</div>
|
|
<!-- VAT Breakdown -->
|
|
<div x-show="order.tax_amount > 0 || order.vat_rate_label" class="flex justify-between text-sm">
|
|
<dt class="text-gray-500 dark:text-gray-400">
|
|
<span x-text="order.vat_rate_label || 'Tax'"></span>
|
|
<span x-show="order.vat_rate" class="text-xs ml-1">(<span x-text="order.vat_rate"></span>%)</span>
|
|
</dt>
|
|
<dd class="font-medium text-gray-900 dark:text-white" x-text="formatPrice(order.tax_amount)"></dd>
|
|
</div>
|
|
<!-- VAT Regime Info (for special cases) -->
|
|
<div x-show="order.vat_regime === 'reverse_charge'" class="text-xs text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-700/50 rounded p-2">
|
|
VAT Reverse Charge applies (B2B transaction)
|
|
</div>
|
|
<div x-show="order.vat_regime === 'exempt'" class="text-xs text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-700/50 rounded p-2">
|
|
VAT Exempt (Non-EU destination)
|
|
</div>
|
|
<div x-show="order.discount_amount > 0" class="flex justify-between text-sm">
|
|
<dt class="text-gray-500 dark:text-gray-400">Discount</dt>
|
|
<dd class="font-medium text-green-600 dark:text-green-400" x-text="'-' + formatPrice(order.discount_amount)"></dd>
|
|
</div>
|
|
<div class="border-t border-gray-200 dark:border-gray-700 pt-3 flex justify-between">
|
|
<dt class="text-base font-semibold text-gray-900 dark:text-white">Total</dt>
|
|
<dd class="text-base font-bold text-gray-900 dark:text-white" x-text="formatPrice(order.total_amount)"></dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
<!-- Invoice Download -->
|
|
<div x-show="canDownloadInvoice()" class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
|
|
<span class="h-5 w-5 mr-2 text-gray-400" x-html="$icon('document-text', 'h-5 w-5')"></span>
|
|
Invoice
|
|
</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
|
Download your invoice for this order.
|
|
</p>
|
|
<button @click="downloadInvoice()"
|
|
:disabled="downloadingInvoice"
|
|
class="w-full inline-flex justify-center items-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm 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 transition-colors">
|
|
<span x-show="!downloadingInvoice" class="h-4 w-4 mr-2" x-html="$icon('arrow-down-tray', 'h-4 w-4')"></span>
|
|
<span x-show="downloadingInvoice" class="h-4 w-4 mr-2" x-html="$icon('spinner', 'h-4 w-4 animate-spin')"></span>
|
|
<span x-text="downloadingInvoice ? 'Generating...' : 'Download Invoice'"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Shipping Info -->
|
|
<div x-show="order.shipping_method || order.tracking_number" class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
|
|
<span class="h-5 w-5 mr-2 text-gray-400" x-html="$icon('archive', 'h-5 w-5')"></span>
|
|
Shipping
|
|
</h3>
|
|
<div class="space-y-3 text-sm">
|
|
<div x-show="order.shipping_method">
|
|
<p class="text-gray-500 dark:text-gray-400">Method</p>
|
|
<p class="font-medium text-gray-900 dark:text-white" x-text="order.shipping_method"></p>
|
|
</div>
|
|
<div x-show="order.shipping_carrier">
|
|
<p class="text-gray-500 dark:text-gray-400">Carrier</p>
|
|
<p class="font-medium text-gray-900 dark:text-white" x-text="order.shipping_carrier"></p>
|
|
</div>
|
|
<div x-show="order.tracking_number">
|
|
<p class="text-gray-500 dark:text-gray-400">Tracking Number</p>
|
|
<p class="font-medium text-gray-900 dark:text-white" x-text="order.tracking_number"></p>
|
|
</div>
|
|
<!-- Track Package Button -->
|
|
<a x-show="order.tracking_url"
|
|
:href="order.tracking_url"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="mt-3 w-full inline-flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white transition-colors"
|
|
style="background-color: var(--color-primary)">
|
|
<span class="h-4 w-4 mr-2" x-html="$icon('location-marker', 'h-4 w-4')"></span>
|
|
Track Package
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Need Help? -->
|
|
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Need Help?</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-300 mb-4">
|
|
If you have any questions about your order, please contact us.
|
|
</p>
|
|
<a href="{{ base_url }}shop/account/messages"
|
|
class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white transition-colors"
|
|
style="background-color: var(--color-primary)">
|
|
<span class="h-4 w-4 mr-2" x-html="$icon('chat-bubble-left', 'h-4 w-4')"></span>
|
|
Contact Support
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Back Button -->
|
|
<div class="mt-8">
|
|
<a href="{{ base_url }}shop/account/orders"
|
|
class="inline-flex items-center text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-primary">
|
|
<span class="h-4 w-4 mr-2" x-html="$icon('chevron-left', 'h-4 w-4')"></span>
|
|
Back to Orders
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
function shopOrderDetailPage() {
|
|
return {
|
|
...shopLayoutData(),
|
|
|
|
// State
|
|
order: null,
|
|
loading: true,
|
|
error: '',
|
|
orderId: {{ order_id }},
|
|
downloadingInvoice: false,
|
|
|
|
// 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' }
|
|
},
|
|
|
|
// Timeline step order for progress tracking
|
|
timelineSteps: ['pending', 'processing', 'shipped', 'delivered'],
|
|
|
|
async init() {
|
|
await this.loadOrder();
|
|
},
|
|
|
|
async loadOrder() {
|
|
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 response = await fetch(`/api/v1/shop/orders/${this.orderId}`, {
|
|
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;
|
|
}
|
|
if (response.status === 404) {
|
|
throw new Error('Order not found');
|
|
}
|
|
throw new Error('Failed to load order details');
|
|
}
|
|
|
|
this.order = await response.json();
|
|
|
|
} catch (err) {
|
|
console.error('Error loading order:', err);
|
|
this.error = err.message || 'Failed to load order';
|
|
} 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
|
|
|
|
formatDateTime(dateStr) {
|
|
if (!dateStr) return '-';
|
|
return new Date(dateStr).toLocaleDateString('de-DE', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
},
|
|
|
|
// ===== Timeline Functions =====
|
|
|
|
/**
|
|
* Get the current step index in the order flow
|
|
*/
|
|
getCurrentStepIndex() {
|
|
if (!this.order) return 0;
|
|
const status = this.order.status;
|
|
|
|
// Handle special statuses
|
|
if (status === 'cancelled' || status === 'refunded') {
|
|
return -1; // Special case
|
|
}
|
|
if (status === 'completed') {
|
|
return 4; // All steps complete
|
|
}
|
|
if (status === 'partially_shipped') {
|
|
return 2; // Between processing and shipped
|
|
}
|
|
|
|
return this.timelineSteps.indexOf(status) + 1;
|
|
},
|
|
|
|
/**
|
|
* Check if a timeline step is complete
|
|
*/
|
|
isStepComplete(step) {
|
|
if (!this.order) return false;
|
|
|
|
const currentIndex = this.getCurrentStepIndex();
|
|
const stepIndex = this.timelineSteps.indexOf(step) + 1;
|
|
|
|
return currentIndex >= stepIndex;
|
|
},
|
|
|
|
/**
|
|
* Get CSS classes for a timeline step
|
|
*/
|
|
getTimelineStepClass(step) {
|
|
if (this.isStepComplete(step)) {
|
|
// Completed step - green
|
|
return 'bg-green-500 text-white';
|
|
} else if (this.order && this.timelineSteps.indexOf(this.order.status) === this.timelineSteps.indexOf(step)) {
|
|
// Current step - primary color with pulse
|
|
return 'bg-blue-500 text-white animate-pulse';
|
|
} else {
|
|
// Future step - gray
|
|
return 'bg-gray-200 dark:bg-gray-600 text-gray-400 dark:text-gray-500';
|
|
}
|
|
},
|
|
|
|
// ===== Invoice Functions =====
|
|
|
|
/**
|
|
* Check if invoice can be downloaded (order must be at least processing)
|
|
*/
|
|
canDownloadInvoice() {
|
|
if (!this.order) return false;
|
|
const invoiceStatuses = ['processing', 'partially_shipped', 'shipped', 'delivered', 'completed'];
|
|
return invoiceStatuses.includes(this.order.status);
|
|
},
|
|
|
|
/**
|
|
* Download invoice PDF for this order
|
|
*/
|
|
async downloadInvoice() {
|
|
if (this.downloadingInvoice) return;
|
|
|
|
this.downloadingInvoice = true;
|
|
try {
|
|
const token = localStorage.getItem('customer_token');
|
|
if (!token) {
|
|
window.location.href = '{{ base_url }}shop/account/login?next=' + encodeURIComponent(window.location.pathname);
|
|
return;
|
|
}
|
|
|
|
const response = await fetch(`/api/v1/shop/orders/${this.orderId}/invoice`, {
|
|
method: 'GET',
|
|
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;
|
|
}
|
|
if (response.status === 404) {
|
|
throw new Error('Invoice not yet available. Please try again later.');
|
|
}
|
|
throw new Error('Failed to download invoice');
|
|
}
|
|
|
|
// Get filename from Content-Disposition header if available
|
|
const contentDisposition = response.headers.get('Content-Disposition');
|
|
let filename = `invoice-${this.order.order_number}.pdf`;
|
|
if (contentDisposition) {
|
|
const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
|
if (match && match[1]) {
|
|
filename = match[1].replace(/['"]/g, '');
|
|
}
|
|
}
|
|
|
|
// Download the blob
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
} catch (err) {
|
|
console.error('Error downloading invoice:', err);
|
|
alert(err.message || 'Failed to download invoice');
|
|
} finally {
|
|
this.downloadingInvoice = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|