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

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