Compare commits

...

2 Commits

Author SHA1 Message Date
b9e8b495b7 Verbessere mobile Eintragserfassung mit Fokus-Ruecksprung.
Nach dem Speichern bleibt der Fokus im Feld Neuer Eintrag und die mobile Ansicht nutzt weniger vertikalen Platz, damit auf iPhone mehr Listeneintraege sichtbar sind.

Made-with: Cursor
2026-04-01 15:15:26 +02:00
bec9b466fc Update OCR-Positionsauswahl nach Uebernahme konsistent.
Bereits uebernommene Positionen bleiben in der OCR-Liste abgewaehlt und manuell demarkierte Zeilen werden pro Bon gespeichert, damit die Auswahl auch nach erneutem OCR erhalten bleibt.

Made-with: Cursor
2026-04-01 12:54:28 +02:00
4 changed files with 56 additions and 7 deletions

View File

@ -115,6 +115,9 @@ class ReceiptScanController extends Controller
if (isset($previousMeta['validated_at'])) {
$rawMeta['validated_at'] = $previousMeta['validated_at'];
}
if (isset($previousMeta['item_selection_state']) && is_array($previousMeta['item_selection_state'])) {
$rawMeta['item_selection_state'] = $previousMeta['item_selection_state'];
}
$nextStoreName = $receiptScan->store_name;
if (trim((string) $nextStoreName) === '' && is_string($ocr['store_name'] ?? null) && trim($ocr['store_name']) !== '') {
@ -182,6 +185,17 @@ class ReceiptScanController extends Controller
$prices = $validated['row_prices'] ?? [];
$qtys = $validated['row_qty'] ?? [];
$take = $validated['row_take'] ?? [];
$selectionState = [];
foreach ($labels as $i => $labelRaw) {
$label = trim((string) $labelRaw);
if ($label === '') {
continue;
}
$priceRaw = trim((string) ($prices[$i] ?? ''));
$qtyRaw = trim((string) ($qtys[$i] ?? ''));
$selectionKey = $this->suggestionRowKey($label, $priceRaw, $qtyRaw);
$selectionState[$selectionKey] = isset($take[$i]);
}
$rows = collect($labels)
->map(function ($label, $i) use ($prices, $qtys, $take) {
@ -285,6 +299,7 @@ class ReceiptScanController extends Controller
$meta = is_array($receiptScan->raw_meta) ? $receiptScan->raw_meta : [];
$meta['validated_items'] = $rows->all();
$meta['validated_at'] = Carbon::now()->toISOString();
$meta['item_selection_state'] = $selectionState;
$receiptScan->update(['raw_meta' => $meta]);
return back()->with(
@ -398,6 +413,11 @@ class ReceiptScanController extends Controller
return trim(preg_replace('/\s+[A-E]\s*$/u', '', $raw) ?? $raw);
}
private function suggestionRowKey(string $label, string $priceRaw, string $quantityRaw): string
{
return mb_strtolower(trim($label)).'|'.trim($priceRaw).'|'.trim($quantityRaw);
}
/**
* @return list<array{label: string, price_raw: string, quantity_raw: string, is_uncertain: bool}>
*/

View File

@ -104,7 +104,9 @@ class ShoppingListController extends Controller
'quantity' => $request->filled('quantity') ? $request->string('quantity')->toString() : $doneItem->quantity,
]);
return back()->with('status', 'Eintrag wurde aus erledigt nach offen uebernommen.');
return back()
->with('status', 'Eintrag wurde aus erledigt nach offen uebernommen.')
->with('focus_new_item', true);
}
ShoppingItem::query()->create([
@ -116,7 +118,9 @@ class ShoppingListController extends Controller
'is_done' => false,
]);
return back()->with('status', 'Eintrag wurde hinzugefuegt.');
return back()
->with('status', 'Eintrag wurde hinzugefuegt.')
->with('focus_new_item', true);
}
public function update(UpdateShoppingItemRequest $request, ShoppingItem $shoppingItem): RedirectResponse

View File

@ -149,6 +149,9 @@
->filter()
->all();
$appliedSet = array_flip($appliedLowerKeys);
$selectionState = is_array($scan->raw_meta['item_selection_state'] ?? null)
? $scan->raw_meta['item_selection_state']
: [];
@endphp
<div class="mt-3 rounded-md border border-blue-200 bg-blue-50 p-3">
<p class="text-xs font-semibold text-blue-900">
@ -177,13 +180,21 @@
$listKey = mb_strtolower(trim($label));
$listStatus = $listProductLookup[$listKey] ?? null;
$wasAppliedFromReceipt = $listKey !== '' && isset($appliedSet[$listKey]);
$selectionKey = $listKey.'|'.trim((string) $priceRaw).'|'.trim((string) $qtyRaw);
$storedSelection = $selectionState[$selectionKey] ?? null;
$defaultChecked = $storedSelection !== null
? (bool) $storedSelection
: (! $isUncertain);
if ($wasAppliedFromReceipt) {
$defaultChecked = false;
}
@endphp
<div class="receipt-suggestion-row flex flex-nowrap items-center gap-2 w-full min-w-0 {{ $isUncertain ? 'opacity-85' : '' }}">
<input
type="checkbox"
name="row_take[{{ $rowIndex }}]"
value="1"
@checked(! $isUncertain)
@checked($defaultChecked)
class="shrink-0 rounded border-gray-300 text-blue-600 shadow-sm h-5 w-5 mt-0.5"
aria-label="Position als erledigt uebernehmen"
title="Uebernehmen"

View File

@ -5,8 +5,8 @@
</h2>
</x-slot>
<div class="py-6">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
<div id="shopping-list-page" data-focus-new-item="{{ session('focus_new_item') ? '1' : '0' }}" class="py-3 sm:py-6">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-4 sm:space-y-6">
<datalist id="store-options">
@foreach($stores as $store)
<option value="{{ $store->name }}"></option>
@ -34,8 +34,8 @@
@endif
<div class="bg-white overflow-hidden shadow-sm sm:rounded-xl p-4 sm:p-6">
<h3 class="text-lg font-semibold mb-3">Neuer Eintrag</h3>
<p class="text-sm text-gray-600 mb-3">Nur Namen eingeben, mit <kbd class="px-1 bg-gray-100 rounded text-xs">Enter</kbd> speichern.</p>
<h3 class="text-lg font-semibold mb-2">Neuer Eintrag</h3>
<p class="hidden sm:block text-sm text-gray-600 mb-3">Nur Namen eingeben, mit <kbd class="px-1 bg-gray-100 rounded text-xs">Enter</kbd> speichern.</p>
<form method="POST" action="{{ route('shopping-items.store') }}" class="flex flex-col sm:flex-row gap-2">
@csrf
<input
@ -364,6 +364,20 @@
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const pageRoot = document.getElementById('shopping-list-page');
const shouldFocusNewItem = pageRoot?.dataset.focusNewItem === '1';
if (!shouldFocusNewItem) {
return;
}
const input = document.getElementById('new-item-name');
if (!input) {
return;
}
input.focus({ preventScroll: true });
input.select();
});
document.addEventListener('click', async (event) => {
const pasteBtn = event.target.closest('.js-paste-price');
if (!pasteBtn) {