feat: integer cents money handling, order page fixes, and vendor filter persistence

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>
This commit is contained in:
2025-12-20 20:33:48 +01:00
parent 7f0d32c18d
commit a19c84ea4e
56 changed files with 6155 additions and 447 deletions

View File

@@ -59,17 +59,21 @@
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 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">
<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">Shipment ID</span>
<span class="font-mono text-xs text-gray-600 dark:text-gray-400" x-text="order?.external_shipment_id"></span>
<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>
@@ -128,28 +132,47 @@
</div>
</div>
<!-- Tracking Information -->
<!-- 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>
Tracking Information
Shipping & Tracking
</h4>
<div x-show="order?.tracking_number" class="space-y-3 text-sm">
<div class="flex justify-between">
<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" x-text="order?.tracking_provider"></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">
<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?.tracking_number" class="text-sm text-gray-500 dark:text-gray-400 italic">
No tracking information available
<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>

View File

@@ -78,73 +78,86 @@
<!-- Error Message -->
{{ error_state('Error', show_condition='error && !loading') }}
<!-- Vendor Required Warning -->
<div x-show="!selectedVendor && !loading" class="mb-8 p-6 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
<!-- Cross-vendor info banner (shown when no vendor selected) -->
<div x-show="!selectedVendor && !loading" class="mb-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
<div class="flex items-center">
<span x-html="$icon('exclamation', 'w-6 h-6 text-yellow-500 mr-3')"></span>
<span x-html="$icon('information-circle', 'w-6 h-6 text-blue-500 mr-3')"></span>
<div>
<h3 class="font-medium text-yellow-800 dark:text-yellow-200">Select a Vendor</h3>
<p class="text-sm text-yellow-700 dark:text-yellow-300">Please select a vendor from the dropdown above to manage their Letzshop integration.</p>
<h3 class="font-medium text-blue-800 dark:text-blue-200">All Vendors View</h3>
<p class="text-sm text-blue-700 dark:text-blue-300">Showing data across all vendors. Select a vendor above to manage products, import orders, or access settings.</p>
</div>
</div>
</div>
<!-- Main Content (shown when vendor selected) -->
<div x-show="selectedVendor" x-transition x-cloak>
<!-- Vendor Info Bar -->
<div class="mb-6 p-4 bg-white dark:bg-gray-800 rounded-lg shadow-xs flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center">
<span class="text-lg font-semibold text-purple-600 dark:text-purple-300" x-text="selectedVendor?.name?.charAt(0).toUpperCase()"></span>
<!-- Main Content -->
<div x-show="!loading" x-transition x-cloak>
<!-- Selected Vendor Filter (same pattern as orders page) -->
<div x-show="selectedVendor" x-transition class="mb-6 p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center">
<span class="text-sm font-semibold text-purple-600 dark:text-purple-300" x-text="selectedVendor?.name?.charAt(0).toUpperCase()"></span>
</div>
<div class="flex items-center gap-3">
<div>
<span class="font-medium text-purple-800 dark:text-purple-200" x-text="selectedVendor?.name"></span>
<span class="ml-2 text-xs text-purple-600 dark:text-purple-400 font-mono" x-text="selectedVendor?.vendor_code"></span>
</div>
<!-- Status badges -->
<span class="px-2 py-0.5 text-xs font-medium rounded-full"
:class="letzshopStatus.is_configured ? 'bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300' : 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400'"
x-text="letzshopStatus.is_configured ? 'Configured' : 'Not Configured'">
</span>
<span x-show="letzshopStatus.auto_sync_enabled" class="px-2 py-0.5 text-xs font-medium rounded-full bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300">
Auto-sync
</span>
</div>
</div>
<div>
<h3 class="font-semibold text-gray-800 dark:text-gray-200" x-text="selectedVendor?.name"></h3>
<p class="text-sm text-gray-500" x-text="selectedVendor?.vendor_code"></p>
</div>
</div>
<div class="flex items-center gap-2">
<!-- Status Badge -->
<span class="px-3 py-1 text-sm font-medium rounded-full"
:class="letzshopStatus.is_configured ? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300' : 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300'"
x-text="letzshopStatus.is_configured ? 'Configured' : 'Not Configured'">
</span>
<!-- Auto-sync indicator -->
<span x-show="letzshopStatus.auto_sync_enabled" class="px-3 py-1 text-sm font-medium rounded-full bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300">
Auto-sync
</span>
<button @click="clearVendorSelection()" class="text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 text-sm flex items-center gap-1">
<span x-html="$icon('x', 'w-4 h-4')"></span>
Clear filter
</button>
</div>
</div>
<!-- Tabs -->
{% call tabs_nav(tab_var='activeTab') %}
{{ tab_button('products', 'Products', tab_var='activeTab', icon='cube') }}
<template x-if="selectedVendor">
<span>{{ tab_button('products', 'Products', tab_var='activeTab', icon='cube') }}</span>
</template>
{{ tab_button('orders', 'Orders', tab_var='activeTab', icon='shopping-cart', count_var='orderStats.pending') }}
{{ tab_button('exceptions', 'Exceptions', tab_var='activeTab', icon='exclamation-circle', count_var='exceptionStats.pending') }}
{{ tab_button('settings', 'Settings', tab_var='activeTab', icon='cog') }}
<template x-if="selectedVendor">
<span>{{ tab_button('settings', 'Settings', tab_var='activeTab', icon='cog') }}</span>
</template>
{% endcall %}
<!-- Products Tab (Import + Export) -->
{{ tab_panel('products', tab_var='activeTab') }}
{% include 'admin/partials/letzshop-products-tab.html' %}
{{ endtab_panel() }}
<!-- Products Tab (Import + Export) - Vendor only -->
<template x-if="selectedVendor">
{{ tab_panel('products', tab_var='activeTab') }}
{% include 'admin/partials/letzshop-products-tab.html' %}
{{ endtab_panel() }}
</template>
<!-- Orders Tab -->
{{ tab_panel('orders', tab_var='activeTab') }}
{% include 'admin/partials/letzshop-orders-tab.html' %}
{{ endtab_panel() }}
<!-- Settings Tab -->
{{ tab_panel('settings', tab_var='activeTab') }}
{% include 'admin/partials/letzshop-settings-tab.html' %}
{{ endtab_panel() }}
<!-- Settings Tab - Vendor only -->
<template x-if="selectedVendor">
{{ tab_panel('settings', tab_var='activeTab') }}
{% include 'admin/partials/letzshop-settings-tab.html' %}
{{ endtab_panel() }}
</template>
<!-- Exceptions Tab -->
{{ tab_panel('exceptions', tab_var='activeTab') }}
{% include 'admin/partials/letzshop-exceptions-tab.html' %}
{{ endtab_panel() }}
<!-- Unified Jobs Table (below all tabs) -->
<div class="mt-8">
<!-- Unified Jobs Table (below all tabs) - Vendor only -->
<div x-show="selectedVendor" class="mt-8">
{% include 'admin/partials/letzshop-jobs-table.html' %}
</div>
</div>
@@ -361,7 +374,7 @@
(<span x-text="selectedOrder?.items?.length"></span> item<span x-show="selectedOrder?.items?.length > 1">s</span>)
</span>
</h4>
<div class="space-y-2">
<div class="space-y-2 max-h-64 overflow-y-auto">
<template x-for="(item, index) in selectedOrder?.items || []" :key="item.id">
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
<div class="flex justify-between items-start">

View File

@@ -59,7 +59,7 @@
<div class="flex items-center gap-4">
<!-- Vendor Autocomplete (Tom Select) -->
<div class="w-80">
<select id="vendor-select" x-ref="vendorSelect" placeholder="All vendors...">
<select id="vendor-select" x-ref="vendorSelect" placeholder="Search vendor...">
</select>
</div>
{{ refresh_button(loading_var='loading', onclick='refresh()', variant='secondary') }}
@@ -401,20 +401,23 @@
<div class="grid grid-cols-2 gap-4">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Customer</p>
<p class="text-sm font-medium text-gray-800 dark:text-gray-200" x-text="selectedOrderDetail?.customer?.first_name + ' ' + selectedOrderDetail?.customer?.last_name"></p>
<p class="text-xs text-gray-500" x-text="selectedOrderDetail?.customer?.email"></p>
<p class="text-sm font-medium text-gray-800 dark:text-gray-200" x-text="(selectedOrderDetail?.customer_first_name || '') + ' ' + (selectedOrderDetail?.customer_last_name || '')"></p>
<p class="text-xs text-gray-500" x-text="selectedOrderDetail?.customer_email"></p>
</div>
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Vendor</p>
<p class="text-sm font-medium text-gray-800 dark:text-gray-200" x-text="selectedOrderDetail?.vendor?.name"></p>
<p class="text-xs text-gray-500 font-mono" x-text="selectedOrderDetail?.vendor?.vendor_code"></p>
<p class="text-sm font-medium text-gray-800 dark:text-gray-200" x-text="selectedOrderDetail?.vendor_name || 'Unknown'"></p>
<p class="text-xs text-gray-500 font-mono" x-text="selectedOrderDetail?.vendor_code || ''"></p>
</div>
</div>
<!-- Items -->
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase mb-2">Items</p>
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg overflow-hidden">
<div class="flex items-center justify-between mb-2">
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Items</p>
<span class="text-xs text-gray-400" x-text="(selectedOrderDetail?.items?.length || 0) + ' item(s)'"></span>
</div>
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg overflow-hidden max-h-48 overflow-y-auto">
<template x-for="item in selectedOrderDetail?.items || []" :key="item.id">
<div class="flex items-center justify-between px-4 py-2 border-b border-gray-200 dark:border-gray-600 last:border-0">
<div>
@@ -457,21 +460,65 @@
</div>
<!-- Shipping Address -->
<div x-show="selectedOrderDetail?.shipping_address">
<div x-show="selectedOrderDetail?.ship_address_line_1">
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase mb-2">Shipping Address</p>
<div class="text-sm text-gray-700 dark:text-gray-300">
<p x-text="selectedOrderDetail?.shipping_address?.first_name + ' ' + selectedOrderDetail?.shipping_address?.last_name"></p>
<p x-text="selectedOrderDetail?.shipping_address?.address_line_1"></p>
<p x-show="selectedOrderDetail?.shipping_address?.address_line_2" x-text="selectedOrderDetail?.shipping_address?.address_line_2"></p>
<p x-text="selectedOrderDetail?.shipping_address?.postal_code + ' ' + selectedOrderDetail?.shipping_address?.city"></p>
<p x-text="selectedOrderDetail?.shipping_address?.country"></p>
<p x-text="(selectedOrderDetail?.ship_first_name || '') + ' ' + (selectedOrderDetail?.ship_last_name || '')"></p>
<p x-text="selectedOrderDetail?.ship_address_line_1"></p>
<p x-show="selectedOrderDetail?.ship_address_line_2" x-text="selectedOrderDetail?.ship_address_line_2"></p>
<p x-text="(selectedOrderDetail?.ship_postal_code || '') + ' ' + (selectedOrderDetail?.ship_city || '')"></p>
<p x-text="selectedOrderDetail?.ship_country_iso"></p>
</div>
</div>
<!-- Tracking Info -->
<div x-show="selectedOrderDetail?.tracking_number">
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase mb-1">Tracking Number</p>
<p class="text-sm font-mono text-gray-800 dark:text-gray-200" x-text="selectedOrderDetail?.tracking_number"></p>
<!-- Shipping & Tracking Info -->
<div x-show="selectedOrderDetail?.shipment_number || selectedOrderDetail?.tracking_number || selectedOrderDetail?.shipping_carrier">
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase mb-2">Shipping & Tracking</p>
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3 space-y-2">
<!-- Shipment Number -->
<div x-show="selectedOrderDetail?.shipment_number" class="flex items-center justify-between">
<div>
<span class="text-xs text-gray-500 dark:text-gray-400">Shipment #:</span>
<span class="ml-2 text-sm font-mono text-gray-800 dark:text-gray-200" x-text="selectedOrderDetail?.shipment_number"></span>
</div>
<!-- Download Label Button -->
<button
x-show="selectedOrderDetail?.shipping_carrier"
@click="downloadShippingLabel(selectedOrderDetail)"
class="flex items-center gap-1 px-2 py-1 text-xs font-medium text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 bg-purple-50 dark:bg-purple-900/30 rounded"
title="Download shipping label"
>
<span x-html="$icon('download', 'w-3 h-3')"></span>
Label
</button>
</div>
<!-- Carrier -->
<div x-show="selectedOrderDetail?.shipping_carrier" class="flex items-center">
<span class="text-xs text-gray-500 dark:text-gray-400">Carrier:</span>
<span class="ml-2 px-2 py-0.5 text-xs font-medium rounded capitalize"
:class="{
'bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200': selectedOrderDetail?.shipping_carrier === 'greco',
'bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200': selectedOrderDetail?.shipping_carrier === 'colissimo',
'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200': selectedOrderDetail?.shipping_carrier === 'xpresslogistics',
'bg-gray-100 dark:bg-gray-600 text-gray-800 dark:text-gray-200': !['greco', 'colissimo', 'xpresslogistics'].includes(selectedOrderDetail?.shipping_carrier)
}"
x-text="selectedOrderDetail?.shipping_carrier"></span>
</div>
<!-- Tracking Number (if different from shipment) -->
<div x-show="selectedOrderDetail?.tracking_number" class="flex items-center">
<span class="text-xs text-gray-500 dark:text-gray-400">Tracking #:</span>
<span class="ml-2 text-sm font-mono text-gray-800 dark:text-gray-200" x-text="selectedOrderDetail?.tracking_number"></span>
</div>
<!-- Tracking URL -->
<div x-show="selectedOrderDetail?.tracking_url" class="flex items-center">
<span class="text-xs text-gray-500 dark:text-gray-400">Track:</span>
<a :href="selectedOrderDetail?.tracking_url" target="_blank"
class="ml-2 text-xs text-purple-600 dark:text-purple-400 hover:underline flex items-center gap-1">
View tracking
<span x-html="$icon('external-link', 'w-3 h-3')"></span>
</a>
</div>
</div>
</div>
<!-- Notes -->
@@ -480,18 +527,113 @@
<p class="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap" x-text="selectedOrderDetail?.internal_notes"></p>
</div>
<div class="flex items-center justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
<button
@click="showDetailModal = false"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
<div class="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-gray-700">
<div>
<!-- Mark as Shipped button (only for processing orders) -->
<button
x-show="selectedOrderDetail?.status === 'processing'"
@click="showDetailModal = false; openMarkAsShippedModal(selectedOrderDetail)"
class="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors"
>
<span x-html="$icon('truck', 'w-4 h-4')"></span>
Mark as Shipped
</button>
</div>
<div class="flex items-center gap-3">
<button
@click="showDetailModal = false"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
>
Close
</button>
<button
@click="showDetailModal = false; openStatusModal(selectedOrderDetail)"
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 transition-colors"
>
Update Status
</button>
</div>
</div>
</div>
{% endcall %}
<!-- Mark as Shipped Modal -->
{% call modal_simple('markAsShippedModal', 'Mark Order as Shipped', show_var='showMarkAsShippedModal', size='sm') %}
<div class="space-y-4">
<p class="text-sm text-gray-600 dark:text-gray-400">
Mark order <span class="font-mono font-medium" x-text="selectedOrder?.order_number"></span> as shipped.
</p>
<!-- Shipment Info (read-only if from Letzshop) -->
<div x-show="selectedOrder?.shipment_number" class="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Letzshop Shipment</p>
<p class="text-sm font-mono text-gray-800 dark:text-gray-200" x-text="selectedOrder?.shipment_number"></p>
<p x-show="selectedOrder?.shipping_carrier" class="text-xs text-gray-500 dark:text-gray-400 mt-1">
Carrier: <span class="capitalize" x-text="selectedOrder?.shipping_carrier"></span>
</p>
</div>
<!-- Tracking Number Input -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tracking Number <span class="text-gray-400">(optional)</span>
</label>
<input
type="text"
x-model="shipForm.tracking_number"
placeholder="e.g., 3XYVi85dDE8l6bov97122"
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-600"
/>
</div>
<!-- Tracking URL Input -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tracking URL <span class="text-gray-400">(optional)</span>
</label>
<input
type="url"
x-model="shipForm.tracking_url"
placeholder="https://dispatchweb.fr/Tracky/Home/..."
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-600"
/>
</div>
<!-- Carrier Select (if not already set) -->
<div x-show="!selectedOrder?.shipping_carrier">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Carrier <span class="text-gray-400">(optional)</span>
</label>
<select
x-model="shipForm.shipping_carrier"
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-600"
>
Close
<option value="">Select carrier...</option>
<option value="greco">Greco</option>
<option value="colissimo">Colissimo</option>
<option value="xpresslogistics">XpressLogistics</option>
<option value="dhl">DHL</option>
<option value="ups">UPS</option>
<option value="fedex">FedEx</option>
<option value="other">Other</option>
</select>
</div>
<div class="flex justify-end gap-3 pt-4">
<button
@click="showMarkAsShippedModal = false"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600"
>
Cancel
</button>
<button
@click="showDetailModal = false; openStatusModal(selectedOrderDetail)"
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 transition-colors"
@click="markAsShipped()"
:disabled="markingAsShipped"
class="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Update Status
<span x-show="!markingAsShipped" x-html="$icon('truck', 'w-4 h-4')"></span>
<span x-show="markingAsShipped" class="animate-spin" x-html="$icon('refresh', 'w-4 h-4')"></span>
<span x-text="markingAsShipped ? 'Shipping...' : 'Mark as Shipped'"></span>
</button>
</div>
</div>

View File

@@ -5,7 +5,7 @@
<div class="flex items-center justify-between mb-6">
<div>
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Product Exceptions</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Resolve unmatched products from order imports</p>
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="selectedVendor ? 'Resolve unmatched products from order imports' : 'All exceptions across vendors'"></p>
</div>
<button
@click="loadExceptions()"
@@ -106,6 +106,7 @@
<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 Info</th>
<th x-show="!selectedVendor" class="px-4 py-3">Vendor</th>
<th class="px-4 py-3">GTIN</th>
<th class="px-4 py-3">Order</th>
<th class="px-4 py-3">Status</th>
@@ -116,7 +117,7 @@
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
<template x-if="loadingExceptions && exceptions.length === 0">
<tr>
<td colspan="6" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<td :colspan="selectedVendor ? 6 : 7" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<span x-html="$icon('spinner', 'w-6 h-6 mx-auto mb-2')"></span>
<p>Loading exceptions...</p>
</td>
@@ -124,7 +125,7 @@
</template>
<template x-if="!loadingExceptions && exceptions.length === 0">
<tr>
<td colspan="6" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<td :colspan="selectedVendor ? 6 : 7" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<span x-html="$icon('check-circle', 'w-12 h-12 mx-auto mb-2 text-green-300')"></span>
<p class="font-medium">No exceptions found</p>
<p class="text-sm mt-1">All order items are properly matched to products</p>
@@ -141,6 +142,10 @@
</div>
</div>
</td>
<!-- Vendor column (only in cross-vendor view) -->
<td x-show="!selectedVendor" class="px-4 py-3 text-sm">
<p class="font-medium text-gray-700 dark:text-gray-200" x-text="exc.vendor_name || 'N/A'"></p>
</td>
<td class="px-4 py-3 text-sm">
<code class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 rounded" x-text="exc.original_gtin || 'No GTIN'"></code>
</td>

View File

@@ -5,9 +5,10 @@
<div class="flex items-center justify-between mb-6">
<div>
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Orders</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Manage Letzshop orders for this vendor</p>
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="selectedVendor ? 'Manage Letzshop orders for this vendor' : 'All Letzshop orders across vendors'"></p>
</div>
<div class="flex gap-2">
<!-- Import buttons only shown when vendor is selected -->
<div x-show="selectedVendor" class="flex gap-2">
<button
@click="importHistoricalOrders()"
:disabled="!letzshopStatus.is_configured || importingHistorical"
@@ -78,9 +79,9 @@
</div>
<!-- Status Cards -->
<div class="grid gap-6 mb-8 md:grid-cols-5">
<!-- Connection Status -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="grid gap-6 mb-8" :class="selectedVendor ? 'md:grid-cols-5' : 'md:grid-cols-4'">
<!-- Connection Status (only when vendor selected) -->
<div x-show="selectedVendor" class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div :class="letzshopStatus.is_configured ? 'bg-green-100 dark:bg-green-900' : 'bg-gray-100 dark:bg-gray-700'" class="p-3 mr-4 rounded-full">
<span x-html="$icon(letzshopStatus.is_configured ? 'check' : 'x', letzshopStatus.is_configured ? 'w-5 h-5 text-green-500' : 'w-5 h-5 text-gray-400')"></span>
</div>
@@ -182,8 +183,8 @@
</button>
</div>
<!-- Not Configured Warning -->
<div x-show="!letzshopStatus.is_configured" class="mb-6 p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
<!-- Not Configured Warning (only when vendor selected) -->
<div x-show="selectedVendor && !letzshopStatus.is_configured" class="mb-6 p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
<div class="flex items-center">
<span x-html="$icon('exclamation', 'w-5 h-5 text-yellow-500 mr-3')"></span>
<div>
@@ -194,12 +195,13 @@
</div>
<!-- Orders Table -->
<div class="w-full overflow-hidden rounded-lg shadow-xs" x-show="letzshopStatus.is_configured">
<div class="w-full overflow-hidden rounded-lg shadow-xs">
<div class="w-full overflow-x-auto">
<table class="w-full whitespace-no-wrap">
<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">Order</th>
<th x-show="!selectedVendor" class="px-4 py-3">Vendor</th>
<th class="px-4 py-3">Customer</th>
<th class="px-4 py-3">Total</th>
<th class="px-4 py-3">Status</th>
@@ -210,7 +212,7 @@
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
<template x-if="loadingOrders && orders.length === 0">
<tr>
<td colspan="6" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<td :colspan="selectedVendor ? 6 : 7" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<span x-html="$icon('spinner', 'w-6 h-6 mx-auto mb-2')"></span>
<p>Loading orders...</p>
</td>
@@ -218,10 +220,10 @@
</template>
<template x-if="!loadingOrders && orders.length === 0">
<tr>
<td colspan="6" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<td :colspan="selectedVendor ? 6 : 7" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<span x-html="$icon('inbox', 'w-12 h-12 mx-auto mb-2 text-gray-300')"></span>
<p class="font-medium">No orders found</p>
<p class="text-sm mt-1">Click "Import Orders" to fetch orders from Letzshop</p>
<p class="text-sm mt-1" x-text="selectedVendor ? 'Click \"Import Orders\" to fetch orders from Letzshop' : 'Select a vendor to import orders'"></p>
</td>
</tr>
</template>
@@ -235,6 +237,10 @@
</div>
</div>
</td>
<!-- Vendor column (only in cross-vendor view) -->
<td x-show="!selectedVendor" class="px-4 py-3 text-sm">
<p class="font-medium text-gray-700 dark:text-gray-200" x-text="order.vendor_name || 'N/A'"></p>
</td>
<td class="px-4 py-3 text-sm">
<p x-text="order.customer_email || 'N/A'"></p>
</td>

View File

@@ -35,6 +35,32 @@
</p>
</div>
<!-- Test Mode -->
<div class="mb-4">
<label class="flex items-center cursor-pointer">
<input
type="checkbox"
x-model="settingsForm.test_mode_enabled"
class="form-checkbox h-5 w-5 text-orange-600 rounded border-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:ring-orange-500"
/>
<span class="ml-3 text-sm font-medium text-gray-700 dark:text-gray-400">Test Mode</span>
</label>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 ml-8">
When enabled, operations (confirm, reject, tracking) will NOT be sent to Letzshop API
</p>
</div>
<!-- Test Mode Warning -->
<div x-show="settingsForm.test_mode_enabled" class="mb-4 p-3 bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg">
<div class="flex items-center">
<span x-html="$icon('exclamation', 'w-5 h-5 text-orange-500 mr-2')"></span>
<span class="text-sm text-orange-700 dark:text-orange-300 font-medium">Test Mode Active</span>
</div>
<p class="mt-1 text-xs text-orange-600 dark:text-orange-400 ml-7">
All Letzshop API mutations are disabled. Orders can be imported but confirmations/rejections will only be saved locally.
</p>
</div>
<!-- Auto Sync -->
<div class="mb-4">
<label class="flex items-center cursor-pointer">
@@ -209,4 +235,106 @@
</div>
</div>
</div>
<!-- Carrier Settings Card -->
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800 lg:col-span-2">
<div class="p-6">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Carrier Settings
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">
Configure default carrier and label URL prefixes for shipping labels.
</p>
<form @submit.prevent="saveCarrierSettings()">
<div class="grid gap-6 lg:grid-cols-2">
<!-- Default Carrier -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
Default Carrier
</label>
<select
x-model="settingsForm.default_carrier"
class="block w-full px-3 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none"
>
<option value="">-- Select carrier --</option>
<option value="greco">Greco</option>
<option value="colissimo">Colissimo</option>
<option value="xpresslogistics">XpressLogistics</option>
</select>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Letzshop automatically assigns carriers based on shipment data
</p>
</div>
<!-- Placeholder for alignment -->
<div></div>
<!-- Greco Label URL -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
<span class="inline-flex items-center">
<span class="w-3 h-3 rounded-full bg-blue-500 mr-2"></span>
Greco Label URL Prefix
</span>
</label>
<input
x-model="settingsForm.carrier_greco_label_url"
type="url"
placeholder="https://dispatchweb.fr/Tracky/Home/"
class="block w-full px-3 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Label URL = Prefix + Shipment Number (e.g., H74683403433)
</p>
</div>
<!-- Colissimo Label URL -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
<span class="inline-flex items-center">
<span class="w-3 h-3 rounded-full bg-yellow-500 mr-2"></span>
Colissimo Label URL Prefix
</span>
</label>
<input
x-model="settingsForm.carrier_colissimo_label_url"
type="url"
placeholder="https://..."
class="block w-full px-3 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none"
/>
</div>
<!-- XpressLogistics Label URL -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
<span class="inline-flex items-center">
<span class="w-3 h-3 rounded-full bg-green-500 mr-2"></span>
XpressLogistics Label URL Prefix
</span>
</label>
<input
x-model="settingsForm.carrier_xpresslogistics_label_url"
type="url"
placeholder="https://..."
class="block w-full px-3 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none"
/>
</div>
</div>
<!-- Save Button -->
<div class="mt-6">
<button
type="submit"
:disabled="savingCarrierSettings"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
>
<span x-show="!savingCarrierSettings" x-html="$icon('save', 'w-4 h-4 mr-2')"></span>
<span x-show="savingCarrierSettings" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
<span x-text="savingCarrierSettings ? 'Saving...' : 'Save Carrier Settings'"></span>
</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -19,6 +19,7 @@
<!-- Settings Categories Tabs -->
{% call tabs_nav() %}
{{ tab_button('logging', 'Logging', icon='document-text') }}
{{ tab_button('shipping', 'Shipping', icon='truck') }}
{{ tab_button('system', 'System', icon='cog') }}
{{ tab_button('security', 'Security', icon='shield-check') }}
{{ tab_button('notifications', 'Notifications', icon='bell') }}
@@ -156,6 +157,102 @@
</div>
</div>
<!-- Shipping Settings Tab -->
<div x-show="activeTab === 'shipping'" x-transition>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-4">
Shipping & Carrier Configuration
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">
Configure shipping carrier label URL prefixes. These are used to generate shipping label download links.
</p>
<!-- Carrier Label URL Settings -->
<div class="space-y-6">
<!-- Greco (Letzshop default) -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<span class="flex items-center gap-2">
<span class="px-2 py-0.5 text-xs font-medium bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded">Greco</span>
Label URL Prefix
</span>
</label>
<input
type="url"
x-model="shippingSettings.carrier_greco_label_url"
placeholder="https://dispatchweb.fr/Tracky/Home/"
class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600"
/>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
The shipment number will be appended to this URL. Default for Letzshop: <code class="bg-gray-100 dark:bg-gray-700 px-1 rounded">https://dispatchweb.fr/Tracky/Home/</code>
</p>
</div>
<!-- Colissimo -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<span class="flex items-center gap-2">
<span class="px-2 py-0.5 text-xs font-medium bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 rounded">Colissimo</span>
Label URL Prefix
</span>
</label>
<input
type="url"
x-model="shippingSettings.carrier_colissimo_label_url"
placeholder="https://www.laposte.fr/outils/suivre-vos-envois?code="
class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600"
/>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
Enter the tracking URL prefix for Colissimo shipments.
</p>
</div>
<!-- XpressLogistics -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<span class="flex items-center gap-2">
<span class="px-2 py-0.5 text-xs font-medium bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded">XpressLogistics</span>
Label URL Prefix
</span>
</label>
<input
type="url"
x-model="shippingSettings.carrier_xpresslogistics_label_url"
placeholder="https://tracking.xpresslogistics.com/"
class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600"
/>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
Enter the tracking URL prefix for XpressLogistics shipments.
</p>
</div>
</div>
<!-- Info Box -->
<div class="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<div class="flex items-start">
<span x-html="$icon('information-circle', 'w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 mr-3 flex-shrink-0')"></span>
<div class="text-sm text-blue-800 dark:text-blue-200">
<p class="font-medium mb-1">How label URLs work</p>
<p>When viewing an order, the system will combine the URL prefix with the shipment number to create a downloadable label link.</p>
<p class="mt-1">Example: <code class="bg-blue-100 dark:bg-blue-800 px-1 rounded">https://dispatchweb.fr/Tracky/Home/</code> + <code class="bg-blue-100 dark:bg-blue-800 px-1 rounded">H74683403433</code></p>
</div>
</div>
</div>
<!-- Save Button -->
<div class="flex items-center justify-end pt-6 mt-6 border-t border-gray-200 dark:border-gray-700">
<button
@click="saveShippingSettings()"
:disabled="saving"
class="px-6 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed"
>
<span x-show="!saving">Save Shipping Settings</span>
<span x-show="saving">Saving...</span>
</button>
</div>
</div>
</div>
<!-- System Settings Tab -->
<div x-show="activeTab === 'system'" x-transition>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">