feat: add item-level order confirmation/decline support

Per Letzshop API, each inventory unit must be confirmed/declined individually.
This enables partial confirmation (some items confirmed, others declined).

Admin API endpoints:
- POST /vendors/{id}/orders/{id}/confirm - confirm all items
- POST /vendors/{id}/orders/{id}/reject - decline all items
- POST /vendors/{id}/orders/{id}/items/{id}/confirm - confirm single item
- POST /vendors/{id}/orders/{id}/items/{id}/decline - decline single item

Order detail modal now shows:
- Product name, EAN, SKU, MPN, price per item
- Per-item state badge (unconfirmed/confirmed/declined)
- Per-item confirm/decline buttons for pending items
- Bulk confirm/decline all buttons

Order status logic:
- If all items declined -> order is "declined"
- If any item confirmed -> order is "confirmed"
- Partial confirmation supported

🤖 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-18 21:27:56 +01:00
parent 12018fc088
commit 3f1f6ad852
4 changed files with 450 additions and 11 deletions

View File

@@ -274,7 +274,7 @@
'bg-red-100 text-red-700': selectedOrder?.sync_status === 'rejected',
'bg-blue-100 text-blue-700': selectedOrder?.sync_status === 'shipped'
}"
x-text="selectedOrder?.sync_status?.toUpperCase()"
x-text="selectedOrder?.sync_status === 'rejected' ? 'DECLINED' : selectedOrder?.sync_status?.toUpperCase()"
></span>
</div>
<div>
@@ -296,19 +296,79 @@
</div>
<div x-show="selectedOrder?.inventory_units?.length > 0">
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-2">Items</h4>
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
<template x-for="unit in selectedOrder?.inventory_units || []" :key="unit.id">
<div class="flex justify-between text-sm py-1 border-b border-gray-200 dark:border-gray-600 last:border-0">
<span class="text-gray-600 dark:text-gray-400" x-text="unit.id"></span>
<span
class="px-2 py-0.5 text-xs rounded-full"
:class="unit.state === 'confirmed' ? 'bg-green-100 text-green-700' : 'bg-orange-100 text-orange-700'"
x-text="unit.state"
></span>
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-2">
Items
<span class="text-xs font-normal text-gray-500">
(Each item must be confirmed or declined individually)
</span>
</h4>
<div class="space-y-2">
<template x-for="(unit, index) in selectedOrder?.inventory_units || []" :key="unit.id">
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
<div class="flex justify-between items-start">
<div class="flex-1">
<p class="font-medium text-gray-700 dark:text-gray-200 text-sm" x-text="unit.product_name || 'Unknown Product'"></p>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1 space-y-0.5">
<p x-show="unit.ean">
<span class="font-medium">EAN:</span> <span x-text="unit.ean"></span>
<span x-show="unit.ean_type" class="text-gray-400" x-text="'(' + unit.ean_type + ')'"></span>
</p>
<p x-show="unit.sku"><span class="font-medium">SKU:</span> <span x-text="unit.sku"></span></p>
<p x-show="unit.mpn"><span class="font-medium">MPN:</span> <span x-text="unit.mpn"></span></p>
<p x-show="unit.price"><span class="font-medium">Price:</span> <span x-text="unit.price + ' EUR'"></span></p>
</div>
</div>
<div class="flex items-center gap-2 ml-4">
<!-- Item State Badge -->
<span
class="px-2 py-0.5 text-xs rounded-full whitespace-nowrap"
:class="{
'bg-orange-100 text-orange-700': unit.state === 'unconfirmed',
'bg-green-100 text-green-700': unit.state === 'confirmed_available' || unit.state === 'confirmed',
'bg-red-100 text-red-700': unit.state === 'confirmed_unavailable',
'bg-gray-100 text-gray-700': unit.state === 'returned'
}"
x-text="unit.state === 'confirmed_unavailable' ? 'DECLINED' : (unit.state === 'confirmed_available' ? 'CONFIRMED' : unit.state?.toUpperCase())"
></span>
<!-- Item Actions (only for unconfirmed items) -->
<template x-if="unit.state === 'unconfirmed' && selectedOrder?.sync_status === 'pending'">
<div class="flex gap-1">
<button
@click="confirmInventoryUnit(selectedOrder, unit, index)"
class="p-1 text-green-600 hover:bg-green-100 rounded"
title="Confirm this item"
>
<span x-html="$icon('check', 'w-4 h-4')"></span>
</button>
<button
@click="declineInventoryUnit(selectedOrder, unit, index)"
class="p-1 text-red-600 hover:bg-red-100 rounded"
title="Decline this item"
>
<span x-html="$icon('x', 'w-4 h-4')"></span>
</button>
</div>
</template>
</div>
</div>
</div>
</template>
</div>
<!-- Bulk Actions -->
<div x-show="selectedOrder?.sync_status === 'pending'" class="mt-4 flex gap-2 justify-end">
<button
@click="confirmAllItems(selectedOrder)"
class="px-3 py-1.5 text-sm text-white bg-green-600 hover:bg-green-700 rounded-lg"
>
Confirm All Items
</button>
<button
@click="declineAllItems(selectedOrder)"
class="px-3 py-1.5 text-sm text-white bg-red-600 hover:bg-red-700 rounded-lg"
>
Decline All Items
</button>
</div>
</div>
</div>
</div>