Teilen mit anderen Benutzern
This commit is contained in:
parent
7306444e35
commit
59cfc18f48
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Concerns;
|
||||
|
||||
use App\Models\ShoppingList;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
trait ResolvesCurrentShoppingList
|
||||
{
|
||||
protected function currentShoppingList(Request $request): ShoppingList
|
||||
{
|
||||
$user = $request->user();
|
||||
abort_if($user === null, 403);
|
||||
|
||||
$lists = $user->accessibleShoppingLists()->with('owner')->orderBy('name')->get();
|
||||
|
||||
if ($lists->isEmpty()) {
|
||||
$list = ShoppingList::query()->create([
|
||||
'owner_id' => $user->id,
|
||||
'name' => 'Einkaufsliste',
|
||||
]);
|
||||
$list->members()->attach($user->id);
|
||||
$list->load('owner');
|
||||
$lists = collect([$list]);
|
||||
}
|
||||
|
||||
$sessionId = $request->session()->get('current_shopping_list_id');
|
||||
$current = $lists->firstWhere('id', $sessionId);
|
||||
|
||||
if ($current === null) {
|
||||
$current = $lists->first();
|
||||
$request->session()->put('current_shopping_list_id', $current->id);
|
||||
}
|
||||
|
||||
return $current;
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
use AuthorizesRequests;
|
||||
}
|
||||
|
||||
@ -2,30 +2,38 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Concerns\ResolvesCurrentShoppingList;
|
||||
use App\Http\Requests\StoreShoppingItemRequest;
|
||||
use App\Http\Requests\ToggleShoppingItemRequest;
|
||||
use App\Http\Requests\UpdateShoppingItemRequest;
|
||||
use App\Models\ItemPriceLog;
|
||||
use App\Models\ShoppingItem;
|
||||
use App\Models\ShoppingList;
|
||||
use App\Models\Store;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Contracts\View\View;
|
||||
|
||||
class ShoppingListController extends Controller
|
||||
{
|
||||
use ResolvesCurrentShoppingList;
|
||||
|
||||
public function index(): View
|
||||
{
|
||||
/** @var \App\Models\User|null $user */
|
||||
$user = auth()->user();
|
||||
if ($user === null) {
|
||||
abort(403);
|
||||
}
|
||||
$request = request();
|
||||
/** @var \App\Models\User $user */
|
||||
$user = $request->user();
|
||||
abort_if($user === null, 403);
|
||||
|
||||
$currentList = $this->currentShoppingList($request);
|
||||
$this->authorize('view', $currentList);
|
||||
|
||||
$accessibleLists = $user->accessibleShoppingLists()->orderBy('name')->get();
|
||||
|
||||
$items = ShoppingItem::query()
|
||||
->where('user_id', $user->id)
|
||||
->with(['store', 'latestPriceLog.store'])
|
||||
->where('shopping_list_id', $currentList->id)
|
||||
->with(['store', 'latestPriceLog.store', 'creator'])
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
@ -41,7 +49,12 @@ class ShoppingListController extends Controller
|
||||
->map(fn ($group) => $group->sum(fn (ShoppingItem $item) => (float) $item->latestPriceLog->price_decimal))
|
||||
->sortDesc();
|
||||
|
||||
$members = $currentList->members()->orderBy('name')->get();
|
||||
|
||||
return view('shopping-list.index', [
|
||||
'currentList' => $currentList,
|
||||
'accessibleLists' => $accessibleLists,
|
||||
'members' => $members,
|
||||
'openItems' => $openItems,
|
||||
'doneItems' => $doneItems,
|
||||
'stores' => $stores,
|
||||
@ -52,6 +65,9 @@ class ShoppingListController extends Controller
|
||||
|
||||
public function store(StoreShoppingItemRequest $request): RedirectResponse
|
||||
{
|
||||
$currentList = $this->currentShoppingList($request);
|
||||
$this->authorize('view', $currentList);
|
||||
|
||||
$storeId = $this->resolveStoreId(
|
||||
$request->integer('store_id') ?: null,
|
||||
$request->input('new_store_name'),
|
||||
@ -59,7 +75,8 @@ class ShoppingListController extends Controller
|
||||
);
|
||||
|
||||
ShoppingItem::query()->create([
|
||||
'user_id' => $request->user()->id,
|
||||
'shopping_list_id' => $currentList->id,
|
||||
'created_by' => $request->user()->id,
|
||||
'product_name' => $request->string('product_name')->toString(),
|
||||
'quantity' => $request->filled('quantity') ? $request->string('quantity')->toString() : null,
|
||||
'store_id' => $storeId,
|
||||
@ -71,9 +88,9 @@ class ShoppingListController extends Controller
|
||||
|
||||
public function update(UpdateShoppingItemRequest $request, ShoppingItem $shoppingItem): RedirectResponse
|
||||
{
|
||||
if ($shoppingItem->user_id !== $request->user()->id) {
|
||||
abort(403);
|
||||
}
|
||||
$currentList = $this->currentShoppingList($request);
|
||||
$this->authorize('view', $currentList);
|
||||
$this->assertItemInList($shoppingItem, $currentList);
|
||||
|
||||
$storeId = $this->resolveStoreId(
|
||||
$request->integer('store_id') ?: null,
|
||||
@ -92,9 +109,9 @@ class ShoppingListController extends Controller
|
||||
|
||||
public function toggle(ToggleShoppingItemRequest $request, ShoppingItem $shoppingItem): RedirectResponse
|
||||
{
|
||||
if ($shoppingItem->user_id !== $request->user()->id) {
|
||||
abort(403);
|
||||
}
|
||||
$currentList = $this->currentShoppingList($request);
|
||||
$this->authorize('view', $currentList);
|
||||
$this->assertItemInList($shoppingItem, $currentList);
|
||||
|
||||
$isDone = $request->boolean('is_done');
|
||||
$storeId = $this->resolveStoreId(
|
||||
@ -114,13 +131,13 @@ class ShoppingListController extends Controller
|
||||
}
|
||||
$shoppingItem->update($payload);
|
||||
|
||||
if (!$isDone) {
|
||||
if (! $isDone) {
|
||||
return;
|
||||
}
|
||||
|
||||
$photoPath = $request->file('photo')?->store('price-photos', 'public');
|
||||
|
||||
if (!$request->filled('price_decimal') && $photoPath === null) {
|
||||
if (! $request->filled('price_decimal') && $photoPath === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -138,6 +155,11 @@ class ShoppingListController extends Controller
|
||||
return back()->with('status', 'Eintrag wurde aktualisiert.');
|
||||
}
|
||||
|
||||
private function assertItemInList(ShoppingItem $shoppingItem, ShoppingList $shoppingList): void
|
||||
{
|
||||
abort_if((int) $shoppingItem->shopping_list_id !== (int) $shoppingList->id, 403);
|
||||
}
|
||||
|
||||
private function resolveStoreId(?int $storeId, ?string $newStoreName, int $userId): ?int
|
||||
{
|
||||
$newStoreName = trim((string) $newStoreName);
|
||||
|
||||
47
app/Http/Controllers/ShoppingListMemberController.php
Normal file
47
app/Http/Controllers/ShoppingListMemberController.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\StoreShoppingListMemberRequest;
|
||||
use App\Models\ShoppingList;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class ShoppingListMemberController extends Controller
|
||||
{
|
||||
public function store(StoreShoppingListMemberRequest $request, ShoppingList $shoppingList): RedirectResponse
|
||||
{
|
||||
$this->authorize('manageMembers', $shoppingList);
|
||||
|
||||
$member = User::query()->where('email', $request->validated('email'))->firstOrFail();
|
||||
|
||||
if ($member->id === $request->user()->id) {
|
||||
return back()->withErrors(['email' => 'Du bist bereits Mitglied dieser Liste.']);
|
||||
}
|
||||
|
||||
if ($shoppingList->members()->whereKey($member->id)->exists()) {
|
||||
return back()->withErrors(['email' => 'Dieser Benutzer ist bereits eingetragen.']);
|
||||
}
|
||||
|
||||
$shoppingList->members()->attach($member->id);
|
||||
|
||||
return back()->with('status', $member->name.' kann die Liste jetzt mitbearbeiten.');
|
||||
}
|
||||
|
||||
public function destroy(ShoppingList $shoppingList, User $user): RedirectResponse
|
||||
{
|
||||
$this->authorize('manageMembers', $shoppingList);
|
||||
|
||||
if ($user->id === $shoppingList->owner_id) {
|
||||
return back()->withErrors(['member' => 'Der Listenbesitzer kann nicht entfernt werden.']);
|
||||
}
|
||||
|
||||
if (! $shoppingList->members()->whereKey($user->id)->exists()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$shoppingList->members()->detach($user->id);
|
||||
|
||||
return back()->with('status', 'Zugriff wurde entfernt.');
|
||||
}
|
||||
}
|
||||
19
app/Http/Controllers/ShoppingListSwitchController.php
Normal file
19
app/Http/Controllers/ShoppingListSwitchController.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\ShoppingList;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ShoppingListSwitchController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, ShoppingList $shoppingList): RedirectResponse
|
||||
{
|
||||
$this->authorize('view', $shoppingList);
|
||||
|
||||
$request->session()->put('current_shopping_list_id', $shoppingList->id);
|
||||
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
}
|
||||
32
app/Http/Requests/StoreShoppingListMemberRequest.php
Normal file
32
app/Http/Requests/StoreShoppingListMemberRequest.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreShoppingListMemberRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'string', 'email', 'max:255', 'exists:users,email'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'email.required' => 'Bitte eine E-Mail-Adresse angeben.',
|
||||
'email.email' => 'Die E-Mail-Adresse ist ungueltig.',
|
||||
'email.exists' => 'Es gibt keinen Benutzer mit dieser E-Mail. Die Person muss sich zuerst registrieren.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -34,4 +34,13 @@ class ItemPriceLog extends Model
|
||||
{
|
||||
return $this->belongsTo(Store::class);
|
||||
}
|
||||
|
||||
public function photoUrl(): ?string
|
||||
{
|
||||
if ($this->photo_path === null || $this->photo_path === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return asset('storage/'.ltrim($this->photo_path, '/'));
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,13 +4,14 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
class ShoppingItem extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'shopping_list_id',
|
||||
'created_by',
|
||||
'product_name',
|
||||
'quantity',
|
||||
'store_id',
|
||||
@ -26,9 +27,14 @@ class ShoppingItem extends Model
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
public function shoppingList(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
return $this->belongsTo(ShoppingList::class);
|
||||
}
|
||||
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function store(): BelongsTo
|
||||
|
||||
41
app/Models/ShoppingList.php
Normal file
41
app/Models/ShoppingList.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class ShoppingList extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'owner_id',
|
||||
'name',
|
||||
];
|
||||
|
||||
public function owner(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'owner_id');
|
||||
}
|
||||
|
||||
public function members(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'shopping_list_user')->withTimestamps();
|
||||
}
|
||||
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(ShoppingItem::class);
|
||||
}
|
||||
|
||||
public function isOwner(User $user): bool
|
||||
{
|
||||
return (int) $this->owner_id === (int) $user->id;
|
||||
}
|
||||
|
||||
public function isMember(User $user): bool
|
||||
{
|
||||
return $this->members()->whereKey($user->id)->exists();
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
@ -50,6 +51,16 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
|
||||
public function shoppingItems(): HasMany
|
||||
{
|
||||
return $this->hasMany(ShoppingItem::class);
|
||||
return $this->hasMany(ShoppingItem::class, 'created_by');
|
||||
}
|
||||
|
||||
public function ownedShoppingLists(): HasMany
|
||||
{
|
||||
return $this->hasMany(ShoppingList::class, 'owner_id');
|
||||
}
|
||||
|
||||
public function accessibleShoppingLists(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(ShoppingList::class, 'shopping_list_user')->withTimestamps();
|
||||
}
|
||||
}
|
||||
|
||||
19
app/Policies/ShoppingListPolicy.php
Normal file
19
app/Policies/ShoppingListPolicy.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\ShoppingList;
|
||||
use App\Models\User;
|
||||
|
||||
class ShoppingListPolicy
|
||||
{
|
||||
public function view(User $user, ShoppingList $shoppingList): bool
|
||||
{
|
||||
return $shoppingList->isMember($user);
|
||||
}
|
||||
|
||||
public function manageMembers(User $user, ShoppingList $shoppingList): bool
|
||||
{
|
||||
return $shoppingList->isOwner($user);
|
||||
}
|
||||
}
|
||||
@ -2,8 +2,12 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\ShoppingList;
|
||||
use App\Models\User;
|
||||
use App\Services\PriceScan\NullPriceScanService;
|
||||
use App\Services\PriceScan\PriceScanServiceInterface;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
@ -24,5 +28,17 @@ class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
// Compatibility for older MariaDB/MySQL index length limits.
|
||||
Schema::defaultStringLength(191);
|
||||
|
||||
Event::listen(Registered::class, function (Registered $event): void {
|
||||
$user = $event->user;
|
||||
if (! $user instanceof User) {
|
||||
return;
|
||||
}
|
||||
$list = ShoppingList::query()->create([
|
||||
'owner_id' => $user->id,
|
||||
'name' => 'Einkaufsliste',
|
||||
]);
|
||||
$list->members()->attach($user->id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"doctrine/dbal": "^4.0",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/tinker": "^2.10.1"
|
||||
},
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('shopping_lists', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('owner_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->string('name')->default('Einkaufsliste');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('shopping_list_user', function (Blueprint $table) {
|
||||
$table->foreignId('shopping_list_id')->constrained('shopping_lists')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->timestamps();
|
||||
|
||||
$table->primary(['shopping_list_id', 'user_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('shopping_list_user');
|
||||
Schema::dropIfExists('shopping_lists');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('shopping_items', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('shopping_list_id')->nullable();
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
});
|
||||
|
||||
$userIds = DB::table('users')->pluck('id');
|
||||
$map = [];
|
||||
|
||||
foreach ($userIds as $userId) {
|
||||
$map[$userId] = DB::table('shopping_lists')->insertGetId([
|
||||
'owner_id' => $userId,
|
||||
'name' => 'Einkaufsliste',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
DB::table('shopping_list_user')->insert([
|
||||
'shopping_list_id' => $map[$userId],
|
||||
'user_id' => $userId,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
$items = DB::table('shopping_items')->select('id', 'user_id')->get();
|
||||
foreach ($items as $item) {
|
||||
if (! isset($map[$item->user_id])) {
|
||||
continue;
|
||||
}
|
||||
DB::table('shopping_items')->where('id', $item->id)->update([
|
||||
'shopping_list_id' => $map[$item->user_id],
|
||||
'created_by' => $item->user_id,
|
||||
]);
|
||||
}
|
||||
|
||||
Schema::table('shopping_items', function (Blueprint $table) {
|
||||
$table->dropForeign(['user_id']);
|
||||
$table->dropColumn('user_id');
|
||||
});
|
||||
|
||||
Schema::table('shopping_items', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('shopping_list_id')->nullable(false)->change();
|
||||
$table->unsignedBigInteger('created_by')->nullable(false)->change();
|
||||
});
|
||||
|
||||
Schema::table('shopping_items', function (Blueprint $table) {
|
||||
$table->foreign('shopping_list_id')->references('id')->on('shopping_lists')->cascadeOnDelete();
|
||||
$table->foreign('created_by')->references('id')->on('users')->nullOnDelete();
|
||||
$table->index(['shopping_list_id', 'is_done']);
|
||||
$table->index(['shopping_list_id', 'store_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('shopping_items', function (Blueprint $table) {
|
||||
$table->dropForeign(['shopping_list_id']);
|
||||
$table->dropForeign(['created_by']);
|
||||
$table->dropIndex(['shopping_list_id', 'is_done']);
|
||||
$table->dropIndex(['shopping_list_id', 'store_id']);
|
||||
});
|
||||
|
||||
Schema::table('shopping_items', function (Blueprint $table) {
|
||||
$table->dropColumn(['shopping_list_id', 'created_by']);
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->index(['user_id', 'is_done']);
|
||||
$table->index(['user_id', 'store_id']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -28,6 +28,78 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-xl p-4 sm:p-6">
|
||||
<h3 class="text-lg font-semibold mb-2">Aktuelle Liste</h3>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
<span class="font-medium text-gray-800">{{ $currentList->name }}</span>
|
||||
@if($currentList->owner_id === auth()->id())
|
||||
<span class="text-gray-500">(du bist Besitzer)</span>
|
||||
@else
|
||||
<span class="text-gray-500">(geteilt von {{ $currentList->owner->name }})</span>
|
||||
@endif
|
||||
</p>
|
||||
@if($accessibleLists->count() > 1)
|
||||
<form method="POST" action="{{ route('shopping-lists.switch', $currentList) }}" id="shopping-list-switch-form" class="flex flex-col sm:flex-row gap-2 items-stretch sm:items-center sm:flex-wrap">
|
||||
@csrf
|
||||
<label for="list-switch" class="text-sm text-gray-600 shrink-0">Wechseln zu:</label>
|
||||
<select
|
||||
id="list-switch"
|
||||
class="rounded-md border-gray-300 text-sm min-h-[44px] max-w-md flex-1"
|
||||
onchange="this.form.action = '{{ url('/shopping-lists') }}/' + this.value + '/switch'; this.form.requestSubmit();"
|
||||
>
|
||||
@foreach($accessibleLists as $list)
|
||||
<option value="{{ $list->id }}" @selected($list->id === $currentList->id)>
|
||||
{{ $list->name }} @if($list->owner_id === auth()->id()) (Besitzer) @else ({{ $list->owner->name }}) @endif
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</form>
|
||||
<p class="text-xs text-gray-500 mt-2">Nach Auswahl wird die Seite neu geladen.</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@can('manageMembers', $currentList)
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-xl p-4 sm:p-6">
|
||||
<h3 class="text-lg font-semibold mb-2">Liste teilen</h3>
|
||||
<p class="text-sm text-gray-600 mb-3">Weitere Personen koennen dieselbe Liste bearbeiten. Sie muessen bereits ein Konto registriert haben.</p>
|
||||
<form method="POST" action="{{ route('shopping-lists.members.store', $currentList) }}" class="flex flex-col sm:flex-row gap-2 mb-6">
|
||||
@csrf
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value="{{ old('email') }}"
|
||||
placeholder="E-Mail-Adresse des Benutzers"
|
||||
class="flex-1 rounded-md border-gray-300 shadow-sm min-h-[44px] text-base"
|
||||
autocomplete="email"
|
||||
>
|
||||
<button type="submit" class="shrink-0 rounded-md bg-indigo-600 text-white px-5 min-h-[44px] text-base font-medium hover:bg-indigo-700">
|
||||
Einladen
|
||||
</button>
|
||||
</form>
|
||||
<h4 class="text-sm font-semibold text-gray-800 mb-2">Mitglieder</h4>
|
||||
<ul class="divide-y divide-gray-100 border border-gray-200 rounded-lg">
|
||||
@foreach($members as $member)
|
||||
<li class="flex flex-wrap items-center justify-between gap-2 px-3 py-2 text-sm">
|
||||
<div>
|
||||
<span class="font-medium text-gray-900">{{ $member->name }}</span>
|
||||
<span class="text-gray-500">{{ $member->email }}</span>
|
||||
@if($member->id === $currentList->owner_id)
|
||||
<span class="ml-2 text-xs text-gray-500">Besitzer</span>
|
||||
@endif
|
||||
</div>
|
||||
@if($member->id !== $currentList->owner_id)
|
||||
<form method="POST" action="{{ route('shopping-lists.members.destroy', [$currentList, $member]) }}" onsubmit="return confirm('Zugriff entfernen?');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="text-red-600 hover:text-red-800 text-sm font-medium">Entfernen</button>
|
||||
</form>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endcan
|
||||
|
||||
<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>
|
||||
@ -80,6 +152,9 @@
|
||||
<label for="open-item-{{ $item->id }}" class="cursor-pointer text-base font-medium leading-snug text-gray-900">
|
||||
{{ $item->product_name }}
|
||||
</label>
|
||||
@if($item->creator && (int) $item->creator->id !== (int) auth()->id())
|
||||
<div class="text-xs text-gray-500 mt-0.5">von {{ $item->creator->name }}</div>
|
||||
@endif
|
||||
@if($item->quantity || $item->store)
|
||||
<div class="mt-0.5 text-sm text-gray-500">
|
||||
@if($item->quantity)
|
||||
@ -127,12 +202,28 @@
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="font-medium text-gray-700 line-through">{{ $item->product_name }}</div>
|
||||
@if($item->creator && (int) $item->creator->id !== (int) auth()->id())
|
||||
<div class="text-xs text-gray-500">von {{ $item->creator->name }}</div>
|
||||
@endif
|
||||
<div class="text-sm text-gray-600">
|
||||
Erledigt: {{ optional($item->done_at)->format('d.m.Y H:i') ?: '-' }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
Letzter Preis: {{ $item->latestPriceLog?->price_decimal !== null ? number_format((float) $item->latestPriceLog->price_decimal, 2, ',', '.') . ' EUR' : '-' }}
|
||||
</div>
|
||||
@if($item->latestPriceLog?->photo_path)
|
||||
<div class="mt-2">
|
||||
<p class="text-xs text-gray-500 mb-1">Kassenbon / Foto</p>
|
||||
<a href="{{ $item->latestPriceLog->photoUrl() }}" target="_blank" rel="noopener noreferrer" class="block">
|
||||
<img
|
||||
src="{{ $item->latestPriceLog->photoUrl() }}"
|
||||
alt="Kassenbon"
|
||||
class="max-h-48 w-full max-w-xs rounded-lg border border-gray-200 object-contain bg-gray-50"
|
||||
loading="lazy"
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
@include('shopping-list.partials.item-pencil-panel', [
|
||||
|
||||
@ -112,6 +112,19 @@
|
||||
bei {{ $item->latestPriceLog->store?->name ?: 'ohne Geschaeft' }}
|
||||
({{ optional($item->latestPriceLog->logged_at)->format('d.m.Y H:i') }})
|
||||
</div>
|
||||
@if($item->latestPriceLog->photo_path)
|
||||
<div class="mt-2">
|
||||
<p class="text-xs text-gray-600 mb-1">Letztes Foto</p>
|
||||
<a href="{{ $item->latestPriceLog->photoUrl() }}" target="_blank" rel="noopener noreferrer" class="block">
|
||||
<img
|
||||
src="{{ $item->latestPriceLog->photoUrl() }}"
|
||||
alt="Kassenbon"
|
||||
class="max-h-36 w-full rounded border border-gray-200 object-contain bg-gray-50"
|
||||
loading="lazy"
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\ShoppingListController;
|
||||
use App\Http\Controllers\ProfileController;
|
||||
use App\Http\Controllers\ShoppingListController;
|
||||
use App\Http\Controllers\ShoppingListMemberController;
|
||||
use App\Http\Controllers\ShoppingListSwitchController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', function () {
|
||||
@ -13,6 +15,10 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::post('/shopping-items', [ShoppingListController::class, 'store'])->name('shopping-items.store');
|
||||
Route::patch('/shopping-items/{shoppingItem}', [ShoppingListController::class, 'update'])->name('shopping-items.update');
|
||||
Route::patch('/shopping-items/{shoppingItem}/toggle', [ShoppingListController::class, 'toggle'])->name('shopping-items.toggle');
|
||||
|
||||
Route::post('/shopping-lists/{shoppingList}/switch', ShoppingListSwitchController::class)->name('shopping-lists.switch');
|
||||
Route::post('/shopping-lists/{shoppingList}/members', [ShoppingListMemberController::class, 'store'])->name('shopping-lists.members.store');
|
||||
Route::delete('/shopping-lists/{shoppingList}/members/{user}', [ShoppingListMemberController::class, 'destroy'])->name('shopping-lists.members.destroy');
|
||||
});
|
||||
|
||||
Route::middleware('auth')->group(function () {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user