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

@@ -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>