Money Handling Architecture: - Store all monetary values as integer cents (€105.91 = 10591) - Add app/utils/money.py with Money class and conversion helpers - Add static/shared/js/money.js for frontend formatting - Update all database models to use _cents columns (Product, Order, etc.) - Update CSV processor to convert prices to cents on import - Add Alembic migration for Float to Integer conversion - Create .architecture-rules/money.yaml with 7 validation rules - Add docs/architecture/money-handling.md documentation Order Details Page Fixes: - Fix customer name showing 'undefined undefined' - use flat field names - Fix vendor info empty - add vendor_name/vendor_code to OrderDetailResponse - Fix shipping address using wrong nested object structure - Enrich order detail API response with vendor info Vendor Filter Persistence Fixes: - Fix orders.js: restoreSavedVendor now sets selectedVendor and filters - Fix orders.js: init() only loads orders if no saved vendor to restore - Fix marketplace-letzshop.js: restoreSavedVendor calls selectVendor() - Fix marketplace-letzshop.js: clearVendorSelection clears TomSelect dropdown - Align vendor selector placeholder text between pages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
316 lines
18 KiB
HTML
316 lines
18 KiB
HTML
{# app/templates/admin/letzshop-order-detail.html #}
|
|
{% extends "admin/base.html" %}
|
|
{% from 'shared/macros/alerts.html' import alert_dynamic, error_state %}
|
|
{% from 'shared/macros/headers.html' import page_header_flex %}
|
|
|
|
{% block title %}Letzshop Order Details{% endblock %}
|
|
{% block alpine_data %}letzshopOrderDetail(){% endblock %}
|
|
|
|
{% block content %}
|
|
<main class="h-full pb-16 overflow-y-auto">
|
|
<div class="container grid px-6 mx-auto">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between my-6">
|
|
<div class="flex items-center gap-4">
|
|
<a
|
|
href="/admin/marketplace/letzshop"
|
|
class="flex items-center text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
|
>
|
|
<span x-html="$icon('arrow-left', 'w-5 h-5')"></span>
|
|
</a>
|
|
<div>
|
|
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
|
|
Order <span x-text="order?.external_order_number || order?.order_number || 'Loading...'"></span>
|
|
</h2>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
Letzshop Order Details
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span
|
|
x-show="order"
|
|
class="px-3 py-1 text-sm rounded-full font-medium"
|
|
:class="{
|
|
'bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300': order?.status === 'pending',
|
|
'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300': order?.status === 'processing',
|
|
'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300': order?.status === 'cancelled',
|
|
'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300': order?.status === 'shipped'
|
|
}"
|
|
x-text="order?.status === 'cancelled' ? 'DECLINED' : (order?.status === 'processing' ? 'CONFIRMED' : order?.status?.toUpperCase())"
|
|
></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div x-show="loading" class="flex justify-center py-12">
|
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
{{ error_state('Failed to load order', 'error') }}
|
|
|
|
<!-- Order Content -->
|
|
<div x-show="order && !loading" class="grid gap-6 mb-8 md:grid-cols-2">
|
|
<!-- Order Information -->
|
|
<div class="min-w-0 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<h4 class="mb-4 font-semibold text-gray-600 dark:text-gray-300 flex items-center gap-2">
|
|
<span x-html="$icon('document-text', 'w-5 h-5')"></span>
|
|
Order Information
|
|
</h4>
|
|
<div class="space-y-3 text-sm">
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500 dark:text-gray-400">Order Date</span>
|
|
<span class="font-medium text-gray-700 dark:text-gray-300" x-text="formatDate(order?.order_date || order?.created_at)"></span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500 dark:text-gray-400">Order Number</span>
|
|
<span class="font-medium text-gray-700 dark:text-gray-300" x-text="order?.external_order_number || order?.order_number"></span>
|
|
</div>
|
|
<div class="flex justify-between" x-show="order?.shipment_number">
|
|
<span class="text-gray-500 dark:text-gray-400">Shipment Number</span>
|
|
<span class="font-mono font-medium text-gray-700 dark:text-gray-300" x-text="order?.shipment_number"></span>
|
|
</div>
|
|
<div class="flex justify-between" x-show="order?.external_shipment_id">
|
|
<span class="text-gray-500 dark:text-gray-400">Hash ID</span>
|
|
<span class="font-mono text-xs text-gray-600 dark:text-gray-400" x-text="order?.external_shipment_id?.split('/').pop()"></span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500 dark:text-gray-400">Total</span>
|
|
<span class="font-semibold text-gray-700 dark:text-gray-300" x-text="order?.total_amount + ' ' + order?.currency"></span>
|
|
</div>
|
|
<div class="flex justify-between" x-show="order?.confirmed_at">
|
|
<span class="text-gray-500 dark:text-gray-400">Confirmed At</span>
|
|
<span class="text-gray-700 dark:text-gray-300" x-text="formatDate(order?.confirmed_at)"></span>
|
|
</div>
|
|
<div class="flex justify-between" x-show="order?.cancelled_at">
|
|
<span class="text-gray-500 dark:text-gray-400">Declined At</span>
|
|
<span class="text-gray-700 dark:text-gray-300" x-text="formatDate(order?.cancelled_at)"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Customer Information -->
|
|
<div class="min-w-0 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<h4 class="mb-4 font-semibold text-gray-600 dark:text-gray-300 flex items-center gap-2">
|
|
<span x-html="$icon('user', 'w-5 h-5')"></span>
|
|
Customer Information
|
|
</h4>
|
|
<div class="space-y-3 text-sm">
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500 dark:text-gray-400">Name</span>
|
|
<span class="font-medium text-gray-700 dark:text-gray-300" x-text="order?.customer_name || 'N/A'"></span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500 dark:text-gray-400">Email</span>
|
|
<a :href="'mailto:' + order?.customer_email" class="text-purple-600 hover:underline" x-text="order?.customer_email"></a>
|
|
</div>
|
|
<div class="flex justify-between" x-show="order?.customer_locale">
|
|
<span class="text-gray-500 dark:text-gray-400">Language</span>
|
|
<span class="uppercase font-medium text-gray-700 dark:text-gray-300" x-text="order?.customer_locale"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Shipping Address -->
|
|
<div class="min-w-0 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800" x-show="order">
|
|
<h4 class="mb-4 font-semibold text-gray-600 dark:text-gray-300 flex items-center gap-2">
|
|
<span x-html="$icon('location-marker', 'w-5 h-5')"></span>
|
|
Shipping Address
|
|
</h4>
|
|
<div class="text-sm text-gray-700 dark:text-gray-300 space-y-1">
|
|
<p 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>
|
|
<p x-show="order?.customer_phone" class="mt-2">
|
|
<span class="text-gray-500 dark:text-gray-400">Phone:</span>
|
|
<span x-text="order?.customer_phone"></span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Shipping & Tracking Information -->
|
|
<div class="min-w-0 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<h4 class="mb-4 font-semibold text-gray-600 dark:text-gray-300 flex items-center gap-2">
|
|
<span x-html="$icon('truck', 'w-5 h-5')"></span>
|
|
Shipping & Tracking
|
|
</h4>
|
|
<div x-show="order?.shipment_number || order?.shipping_carrier || order?.tracking_number || order?.tracking_url" class="space-y-3 text-sm">
|
|
<div class="flex justify-between" x-show="order?.shipping_carrier">
|
|
<span class="text-gray-500 dark:text-gray-400">Carrier</span>
|
|
<span class="font-medium text-gray-700 dark:text-gray-300 capitalize" x-text="order?.shipping_carrier"></span>
|
|
</div>
|
|
<div class="flex justify-between" x-show="order?.shipment_number">
|
|
<span class="text-gray-500 dark:text-gray-400">Shipment Number</span>
|
|
<span class="font-mono text-gray-700 dark:text-gray-300" x-text="order?.shipment_number"></span>
|
|
</div>
|
|
<div class="flex justify-between" x-show="order?.tracking_number">
|
|
<span class="text-gray-500 dark:text-gray-400">Tracking Number</span>
|
|
<span class="font-mono text-gray-700 dark:text-gray-300" x-text="order?.tracking_number"></span>
|
|
</div>
|
|
<div class="flex justify-between" x-show="order?.tracking_provider">
|
|
<span class="text-gray-500 dark:text-gray-400">Tracking Provider</span>
|
|
<span class="text-gray-700 dark:text-gray-300" x-text="order?.tracking_provider"></span>
|
|
</div>
|
|
<div class="flex justify-between" x-show="order?.shipped_at">
|
|
<span class="text-gray-500 dark:text-gray-400">Shipped At</span>
|
|
<span class="text-gray-700 dark:text-gray-300" x-text="formatDate(order?.shipped_at)"></span>
|
|
</div>
|
|
<!-- Tracking Link -->
|
|
<div x-show="order?.tracking_url || (order?.shipping_carrier === 'greco' && order?.shipment_number)" class="pt-2 border-t dark:border-gray-700">
|
|
<a
|
|
:href="order?.tracking_url || ('https://dispatchweb.fr/Tracky/Home/' + order?.shipment_number)"
|
|
target="_blank"
|
|
class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-purple-600 bg-purple-50 dark:bg-purple-900/30 dark:text-purple-400 rounded-lg hover:bg-purple-100 dark:hover:bg-purple-900/50 transition-colors"
|
|
>
|
|
<span x-html="$icon('external-link', 'w-4 h-4')"></span>
|
|
View Tracking / Download Label
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div x-show="!order?.shipment_number && !order?.shipping_carrier && !order?.tracking_number && !order?.tracking_url" class="text-sm text-gray-500 dark:text-gray-400 italic">
|
|
No shipping information available yet
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Order Items -->
|
|
<div x-show="order && !loading" class="min-w-0 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800 mb-8">
|
|
<h4 class="mb-4 font-semibold text-gray-600 dark:text-gray-300 flex items-center gap-2">
|
|
<span x-html="$icon('shopping-bag', 'w-5 h-5')"></span>
|
|
Order Items
|
|
<span class="text-sm font-normal text-gray-500">
|
|
(<span x-text="order?.items?.length || 0"></span> items)
|
|
</span>
|
|
</h4>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full whitespace-nowrap">
|
|
<thead>
|
|
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
|
|
<th class="px-4 py-3">Product</th>
|
|
<th class="px-4 py-3">GTIN/SKU</th>
|
|
<th class="px-4 py-3">Qty</th>
|
|
<th class="px-4 py-3">Price</th>
|
|
<th class="px-4 py-3">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
|
<template x-for="item in order?.items || []" :key="item.id">
|
|
<tr class="text-gray-700 dark:text-gray-400">
|
|
<td class="px-4 py-3">
|
|
<div class="flex items-center">
|
|
<!-- Placeholder for product image -->
|
|
<div class="w-10 h-10 rounded bg-gray-200 dark:bg-gray-600 flex items-center justify-center mr-3">
|
|
<span x-html="$icon('photograph', 'w-5 h-5 text-gray-400')"></span>
|
|
</div>
|
|
<div>
|
|
<p class="font-semibold text-gray-800 dark:text-gray-200" x-text="item.product_name || 'Unknown Product'"></p>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm">
|
|
<p x-show="item.gtin" class="font-mono">
|
|
<span x-text="item.gtin"></span>
|
|
<span x-show="item.gtin_type" class="text-xs text-gray-400" x-text="'(' + item.gtin_type + ')'"></span>
|
|
</p>
|
|
<p x-show="item.product_sku" class="text-xs text-gray-500">
|
|
SKU: <span x-text="item.product_sku"></span>
|
|
</p>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm">
|
|
<span x-text="item.quantity"></span>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm font-medium">
|
|
<span x-text="item.unit_price ? (item.unit_price + ' ' + order?.currency) : 'N/A'"></span>
|
|
<p x-show="item.quantity > 1" class="text-xs text-gray-500">
|
|
Total: <span x-text="item.total_price + ' ' + order?.currency"></span>
|
|
</p>
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
<span
|
|
class="px-2 py-1 text-xs rounded-full font-medium"
|
|
:class="{
|
|
'bg-orange-100 text-orange-700': !item.item_state,
|
|
'bg-green-100 text-green-700': item.item_state === 'confirmed_available',
|
|
'bg-red-100 text-red-700': item.item_state === 'confirmed_unavailable',
|
|
'bg-gray-100 text-gray-700': item.item_state === 'returned'
|
|
}"
|
|
x-text="item.item_state === 'confirmed_unavailable' ? 'DECLINED' : (item.item_state === 'confirmed_available' ? 'CONFIRMED' : (item.item_state ? item.item_state.toUpperCase() : 'PENDING'))"
|
|
></span>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Raw Order Data (collapsible) -->
|
|
<div x-show="order && !loading" class="min-w-0 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800 mb-8">
|
|
<button
|
|
@click="showRawData = !showRawData"
|
|
class="w-full flex items-center justify-between text-left"
|
|
>
|
|
<h4 class="font-semibold text-gray-600 dark:text-gray-300 flex items-center gap-2">
|
|
<span x-html="$icon('code', 'w-5 h-5')"></span>
|
|
Raw Marketplace Data
|
|
</h4>
|
|
<span x-html="showRawData ? $icon('chevron-up', 'w-5 h-5 text-gray-500') : $icon('chevron-down', 'w-5 h-5 text-gray-500')"></span>
|
|
</button>
|
|
<div x-show="showRawData" class="mt-4">
|
|
<pre class="text-xs bg-gray-100 dark:bg-gray-900 p-4 rounded overflow-x-auto max-h-96"><code x-text="JSON.stringify(order?.external_data, null, 2)"></code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
function letzshopOrderDetail() {
|
|
return {
|
|
...data(),
|
|
currentPage: 'marketplace-letzshop',
|
|
orderId: {{ order_id }},
|
|
order: null,
|
|
loading: true,
|
|
error: null,
|
|
showRawData: false,
|
|
|
|
async init() {
|
|
await this.loadOrder();
|
|
},
|
|
|
|
async loadOrder() {
|
|
this.loading = true;
|
|
this.error = null;
|
|
|
|
try {
|
|
// Fetch the order detail from the API
|
|
const response = await apiClient.get(`/admin/letzshop/orders/${this.orderId}`);
|
|
this.order = response;
|
|
} catch (err) {
|
|
console.error('Failed to load order:', err);
|
|
this.error = err.message || 'Failed to load order details';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
formatDate(dateString) {
|
|
if (!dateString) return 'N/A';
|
|
try {
|
|
return new Date(dateString).toLocaleString();
|
|
} catch {
|
|
return dateString;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
{% endblock %}
|