Erweiterung der Datenbank für tatsächliche Profile mit angeboten statt hardcoded. Chat funktionalität auf Käufer erweitert.

This commit is contained in:
serialexperiments 2026-06-30 13:38:48 +02:00
parent 93fe6fa5ca
commit 37665ecf3e
13 changed files with 371 additions and 212 deletions

View file

@ -1,8 +1,10 @@
<?php <?php
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\User;
use App\Models\Message; use App\Models\Message;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class MessageController extends Controller class MessageController extends Controller
{ {
@ -26,4 +28,59 @@ public function store(Request $request)
])); ]));
return response()->json($msg, 201); return response()->json($msg, 201);
} }
public function myConversations(Request $request)
{
$token = $request->bearerToken();
$user = $token ? User::where('api_token', $token)->first() : null;
if (!$user) {
return response()->json(['message' => 'Nicht eingeloggt.'], 401);
}
$role = $request->query('role', 'buyer');
if ($role === 'seller') {
$provider = DB::table('providers')->where('user_id', $user->id)->first();
if (!$provider) {
return response()->json([]);
}
$filterColumn = 'provider_id';
$filterValue = $provider->id;
} else {
$filterColumn = 'buyer_id';
$filterValue = $user->id;
}
$threads = DB::table('messages')
->select('product_id', 'provider_id', 'buyer_id')
->where($filterColumn, $filterValue)
->distinct()
->get();
$result = $threads->map(function ($t) {
$last = DB::table('messages')
->where('product_id', $t->product_id)
->where('buyer_id', $t->buyer_id)
->orderByDesc('created_at')
->first();
$product = DB::table('products')->find($t->product_id);
$provider = DB::table('providers')->find($t->provider_id);
return [
'product_id' => $t->product_id,
'product_title' => $product->title ?? null,
'provider_id' => $t->provider_id,
'provider_name' => $provider->name ?? null,
'buyer_id' => $t->buyer_id,
'last_message' => $last->body ?? null,
'last_sender' => $last->sender ?? null,
'last_at' => $last->created_at ?? null,
];
})->sortByDesc('last_at')->values();
return response()->json($result);
}
} }

View file

@ -65,27 +65,36 @@ public function index(Request $request) {
return response()->json($results); return response()->json($results);
} }
public function show(int $id) { public function show(int $id)
$product = DB::table('products') {
->join('providers', 'products.provider_id', '=', 'providers.id') $product = DB::table('products')
->select('products.*', 'providers.name as provider', 'providers.id as provider_id', ->join('providers', 'products.provider_id', '=', 'providers.id')
'providers.address', 'providers.city', 'providers.zip', ->where('products.id', $id)
'providers.type as provider_type', 'providers.verified', ->select(
'providers.since', 'providers.initials') 'products.id as id',
->where('products.id', $id) 'products.title', 'products.price', 'products.icon', 'products.category',
->first(); 'products.description', 'products.stock', 'products.provider_id',
'providers.name as provider', 'providers.type as provider_type',
'providers.address', 'providers.city', 'providers.verified', 'providers.initials'
)
->first();
if (!$product) return response()->json(['error' => 'Not found'], 404); if (!$product) {
return response()->json(['message' => 'Produkt nicht gefunden.'], 404);
$alternatives = DB::table('products')
->join('providers', 'products.provider_id', '=', 'providers.id')
->select('products.id', 'products.title', 'products.price', 'providers.name as provider')
->where('products.category', $product->category)
->where('products.id', '!=', $id)
->where('products.provider_id', '!=', $product->provider_id) // <- das fehlte
->orderBy('products.price')
->get();
return response()->json(['product' => $product, 'alternatives' => $alternatives]);
} }
$coreWord = explode(' ', trim($product->title))[0];
$alternatives = DB::table('products')
->join('providers', 'products.provider_id', '=', 'providers.id')
->select('products.id', 'products.title', 'products.price', 'providers.name as provider')
->where('products.category', $product->category)
->where('products.id', '!=', $id)
->where('products.provider_id', '!=', $product->provider_id)
->where('products.title', 'like', "%{$coreWord}%")
->orderBy('products.price')
->get();
return response()->json(['product' => $product, 'alternatives' => $alternatives]);
}
} }

View file

@ -8,19 +8,18 @@ public function up(): void {
Schema::create('providers', function (Blueprint $table) { Schema::create('providers', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('name'); $table->string('name');
$table->string('initials', 4); $table->string('initials')->nullable();
$table->string('address'); $table->string('address')->nullable();
$table->string('city'); $table->string('city')->nullable();
$table->string('zip', 10); $table->string('zip')->nullable();
$table->decimal('lat', 10, 7); $table->double('lat')->nullable();
$table->decimal('lng', 10, 7); $table->double('lng')->nullable();
$table->enum('type', ['Fachhandel', 'Baumarkt']); $table->string('type')->nullable();
$table->unsignedSmallInteger('since'); $table->integer('since')->nullable();
$table->boolean('verified')->default(false); $table->boolean('verified')->default(0);
$table->timestamps(); $table->timestamps();
}); });
} }
public function down(): void { public function down(): void {
Schema::dropIfExists('providers'); Schema::dropIfExists('providers');
} }

View file

@ -9,15 +9,14 @@ public function up(): void {
$table->id(); $table->id();
$table->foreignId('provider_id')->constrained()->cascadeOnDelete(); $table->foreignId('provider_id')->constrained()->cascadeOnDelete();
$table->string('title'); $table->string('title');
$table->decimal('price', 8, 2); $table->decimal('price', 10, 2);
$table->string('icon', 8); $table->string('icon')->nullable();
$table->string('category'); $table->string('category')->nullable();
$table->text('description'); $table->text('description')->nullable();
$table->unsignedInteger('stock')->default(0); $table->integer('stock')->default(0);
$table->timestamps(); $table->timestamps();
}); });
} }
public function down(): void { public function down(): void {
Schema::dropIfExists('products'); Schema::dropIfExists('products');
} }

View file

@ -3,6 +3,7 @@
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
class DatabaseSeeder extends Seeder { class DatabaseSeeder extends Seeder {
public function run(): void { public function run(): void {
@ -10,9 +11,35 @@ public function run(): void {
DB::table('products')->truncate(); DB::table('products')->truncate();
DB::table('providers')->truncate(); DB::table('providers')->truncate();
DB::table('users')->truncate();
DB::statement('PRAGMA foreign_keys = ON'); DB::statement('PRAGMA foreign_keys = ON');
// --- Seller-Accounts (User je Provider, gleiche Reihenfolge wie unten) ---
$sellerEmails = [
'mueller@baumarkt-test.de', 'pw@profiwerkzeug-test.de', 'koch@holzfachhandel-test.de',
'lange@farbenwelt-test.de', 'info@baustoffzentrum-test.de', 'depot@werkzeugdepot-test.de',
'krause@sanitaer-test.de', 'goertz@elektro-test.de', 'rheinland@baumarkt-test.de',
'mehr@holzundmehr-test.de', 'fischer@farben-test.de', 'bergisch@baudiscount-test.de',
];
$userIds = [];
foreach ($sellerEmails as $i => $email) {
$userIds[] = DB::table('users')->insertGetId([
'first_name' => 'Verkäufer',
'last_name' => 'Test ' . ($i + 1),
'name' => 'Verkäufer Test ' . ($i + 1),
'email' => $email,
'password' => Hash::make('Passwort123!'),
'role' => 'seller',
'email_verified_at' => now(),
'verification_code' => null,
'created_at' => now(),
'updated_at' => now(),
]);
}
// Login für alle Test-Seller: <email oben> / Passwort123!
$providers = [ $providers = [
['name'=>'Baumarkt Müller', 'initials'=>'BM','address'=>'Musterstraße 12', 'city'=>'Wuppertal', 'zip'=>'42103','lat'=>51.2562,'lng'=>7.1508,'type'=>'Baumarkt', 'since'=>1998,'verified'=>1], ['name'=>'Baumarkt Müller', 'initials'=>'BM','address'=>'Musterstraße 12', 'city'=>'Wuppertal', 'zip'=>'42103','lat'=>51.2562,'lng'=>7.1508,'type'=>'Baumarkt', 'since'=>1998,'verified'=>1],
['name'=>'Profi-Werkzeug GmbH', 'initials'=>'PW','address'=>'Industrieweg 5', 'city'=>'Wuppertal', 'zip'=>'42107','lat'=>51.2637,'lng'=>7.1362,'type'=>'Fachhandel','since'=>2005,'verified'=>1], ['name'=>'Profi-Werkzeug GmbH', 'initials'=>'PW','address'=>'Industrieweg 5', 'city'=>'Wuppertal', 'zip'=>'42107','lat'=>51.2637,'lng'=>7.1362,'type'=>'Fachhandel','since'=>2005,'verified'=>1],
@ -28,68 +55,167 @@ public function run(): void {
['name'=>'Bau-Discount Bergisch', 'initials'=>'BD','address'=>'Hauptstraße 100', 'city'=>'Remscheid', 'zip'=>'42853','lat'=>51.1850,'lng'=>7.1950,'type'=>'Baumarkt', 'since'=>2012,'verified'=>1], ['name'=>'Bau-Discount Bergisch', 'initials'=>'BD','address'=>'Hauptstraße 100', 'city'=>'Remscheid', 'zip'=>'42853','lat'=>51.1850,'lng'=>7.1950,'type'=>'Baumarkt', 'since'=>2012,'verified'=>1],
]; ];
foreach ($providers as $p) { $providerIds = [];
DB::table('providers')->insert(array_merge($p, ['created_at'=>now(),'updated_at'=>now()])); foreach ($providers as $i => $p) {
$providerIds[] = DB::table('providers')->insertGetId(array_merge($p, [
'user_id' => $userIds[$i],
'created_at' => now(),
'updated_at' => now(),
]));
} }
// Helper: $pid($n) gibt die echte DB-id des n-ten Providers zurück (1-indexiert wie vorher)
$pid = fn($n) => $providerIds[$n - 1];
$products = [ $products = [
// Baumaterial // Baumaterial — je Artikel min. 1 Alternative bei anderem Anbieter
['provider_id'=>1, 'title'=>'Klinker-Restposten', 'price'=>89.00, 'icon'=>'🧱','category'=>'Baumaterial', 'description'=>'Ca. 200 Stück verfügbar. Geeignet für Außenwände und Gartenmauern.','stock'=>200], ['provider_id'=>$pid(1), 'title'=>'Klinker-Restposten', 'price'=>89.00, 'icon'=>'🧱','category'=>'Baumaterial','description'=>'Ca. 200 Stück verfügbar. Geeignet für Außenwände und Gartenmauern.','stock'=>200],
['provider_id'=>1, 'title'=>'Betonmischer 140L', 'price'=>180.00, 'icon'=>'🏗️','category'=>'Baumaterial', 'description'=>'Elektrischer Betonmischer 140L, 500W. Einwandfreier Zustand.','stock'=>1], ['provider_id'=>$pid(9), 'title'=>'Klinker Sortiment rot', 'price'=>82.50, 'icon'=>'🧱','category'=>'Baumaterial','description'=>'Klinkersteine, rot gebrannt. 180 Stück verfügbar.','stock'=>180],
['provider_id'=>5, 'title'=>'Pflastersteine grau 10x10', 'price'=>0.89, 'icon'=>'🧱','category'=>'Baumaterial', 'description'=>'Betonpflaster, 6cm stark. Abnahme ab 100 Stück. Restposten.','stock'=>850],
['provider_id'=>5, 'title'=>'Dachziegel Tonrot', 'price'=>1.20, 'icon'=>'🏠','category'=>'Baumaterial', 'description'=>'Gebrauchte Dachziegel, gereinigt. Ca. 300 Stück.','stock'=>300], ['provider_id'=>$pid(1), 'title'=>'Betonmischer 140L', 'price'=>180.00, 'icon'=>'🏗️','category'=>'Baumaterial','description'=>'Elektrischer Betonmischer 140L, 500W. Einwandfreier Zustand.','stock'=>1],
['provider_id'=>9, 'title'=>'Ytong-Steine 20cm', 'price'=>3.50, 'icon'=>'🧱','category'=>'Baumaterial', 'description'=>'Porenbetonsteine, Restpalette. 40 Stück.','stock'=>40], ['provider_id'=>$pid(12),'title'=>'Betonmischer 125L gebraucht','price'=>149.00, 'icon'=>'🏗️','category'=>'Baumaterial','description'=>'Elektrischer Betonmischer 125L. Gebraucht, voll funktionsfähig.','stock'=>2],
['provider_id'=>9, 'title'=>'Betonpflaster Anthrazit', 'price'=>1.10, 'icon'=>'🧱','category'=>'Baumaterial', 'description'=>'Verlegeformat 20x10cm, 6cm. 500 Stück.','stock'=>500],
['provider_id'=>12,'title'=>'Zementsäcke 25kg', 'price'=>6.50, 'icon'=>'🏗️','category'=>'Baumaterial', 'description'=>'Portlandzement CEM I. 30 Säcke verfügbar.','stock'=>30], ['provider_id'=>$pid(5), 'title'=>'Pflastersteine grau 10x10', 'price'=>0.89, 'icon'=>'🧱','category'=>'Baumaterial','description'=>'Betonpflaster, 6cm stark. Abnahme ab 100 Stück. Restposten.','stock'=>850],
['provider_id'=>12,'title'=>'Kalksandstein KS-L', 'price'=>2.80, 'icon'=>'🧱','category'=>'Baumaterial', 'description'=>'20x20x10cm. Restposten 150 Stück.','stock'=>150], ['provider_id'=>$pid(9), 'title'=>'Pflastersteine grau 10x20', 'price'=>0.95, 'icon'=>'🧱','category'=>'Baumaterial','description'=>'Betonpflaster, 8cm stark. Geringe Lagermenge.','stock'=>200],
['provider_id'=>$pid(5), 'title'=>'Dachziegel Tonrot', 'price'=>1.20, 'icon'=>'🏠','category'=>'Baumaterial','description'=>'Gebrauchte Dachziegel, gereinigt. Ca. 300 Stück.','stock'=>300],
['provider_id'=>$pid(1), 'title'=>'Dachziegel Tonrot Neu', 'price'=>1.65, 'icon'=>'🏠','category'=>'Baumaterial','description'=>'Neue Dachziegel, Tonrot. Palettenware.','stock'=>120],
['provider_id'=>$pid(9), 'title'=>'Ytong-Steine 20cm', 'price'=>3.50, 'icon'=>'🧱','category'=>'Baumaterial','description'=>'Porenbetonsteine, Restpalette. 40 Stück.','stock'=>40],
['provider_id'=>$pid(5), 'title'=>'Ytong-Steine 24cm', 'price'=>3.95, 'icon'=>'🧱','category'=>'Baumaterial','description'=>'Porenbetonsteine, stärkere Variante. 30 Stück.','stock'=>30],
['provider_id'=>$pid(9), 'title'=>'Betonpflaster Anthrazit', 'price'=>1.10, 'icon'=>'🧱','category'=>'Baumaterial','description'=>'Verlegeformat 20x10cm, 6cm. 500 Stück.','stock'=>500],
['provider_id'=>$pid(12),'title'=>'Betonpflaster Anthrazit XL', 'price'=>1.25, 'icon'=>'🧱','category'=>'Baumaterial','description'=>'Großformat 30x15cm, 8cm. 250 Stück.','stock'=>250],
['provider_id'=>$pid(12),'title'=>'Zementsäcke 25kg', 'price'=>6.50, 'icon'=>'🏗️','category'=>'Baumaterial','description'=>'Portlandzement CEM I. 30 Säcke verfügbar.','stock'=>30],
['provider_id'=>$pid(1), 'title'=>'Zementsäcke 25kg Schnellbinder', 'price'=>7.80, 'icon'=>'🏗️','category'=>'Baumaterial','description'=>'Schnellbindender Zement. 20 Säcke verfügbar.','stock'=>20],
['provider_id'=>$pid(12),'title'=>'Kalksandstein KS-L', 'price'=>2.80, 'icon'=>'🧱','category'=>'Baumaterial','description'=>'20x20x10cm. Restposten 150 Stück.','stock'=>150],
['provider_id'=>$pid(9), 'title'=>'Kalksandstein KS-L 24cm', 'price'=>3.10, 'icon'=>'🧱','category'=>'Baumaterial','description'=>'24x24x10cm, höhere Tragfähigkeit. 90 Stück.','stock'=>90],
// Werkzeug // Werkzeug
['provider_id'=>2, 'title'=>'Bohrmaschine 18V', 'price'=>64.00, 'icon'=>'⚙️','category'=>'Werkzeug', 'description'=>'Akkubohrmaschine 18V, 2 Akkus, Ladegerät inkl. Leichte Gebrauchsspuren.','stock'=>3], ['provider_id'=>$pid(2), 'title'=>'Bohrmaschine 18V', 'price'=>64.00, 'icon'=>'⚙️','category'=>'Werkzeug','description'=>'Akkubohrmaschine 18V, 2 Akkus, Ladegerät inkl. Leichte Gebrauchsspuren.','stock'=>3],
['provider_id'=>2, 'title'=>'Winkelschleifer 125mm', 'price'=>35.00, 'icon'=>'🔧','category'=>'Werkzeug', 'description'=>'900W, inkl. 5 Trennscheiben. Gebraucht, technisch einwandfrei.','stock'=>2], ['provider_id'=>$pid(6), 'title'=>'Bohrmaschine 18V Pro', 'price'=>72.00, 'icon'=>'⚙️','category'=>'Werkzeug','description'=>'Akkubohrmaschine 18V, bürstenlos, 1 Akku. Neuwertig.','stock'=>5],
['provider_id'=>6, 'title'=>'Kreissäge 1400W', 'price'=>95.00, 'icon'=>'🪚','category'=>'Werkzeug', 'description'=>'185mm Sägeblatt, Parallelanschlag. Guter Zustand.','stock'=>1],
['provider_id'=>6, 'title'=>'Stichsäge Bosch PST', 'price'=>42.00, 'icon'=>'🔧','category'=>'Werkzeug', 'description'=>'650W, inkl. 3 Sägeblätter. Wenig genutzt.','stock'=>2], ['provider_id'=>$pid(2), 'title'=>'Winkelschleifer 125mm', 'price'=>35.00, 'icon'=>'🔧','category'=>'Werkzeug','description'=>'900W, inkl. 5 Trennscheiben. Gebraucht, technisch einwandfrei.','stock'=>2],
['provider_id'=>6, 'title'=>'Schlagbohrmaschine 750W', 'price'=>38.00, 'icon'=>'⚙️','category'=>'Werkzeug', 'description'=>'Kabelgebunden, 13mm Bohrfutter. Sehr guter Zustand.','stock'=>4], ['provider_id'=>$pid(6), 'title'=>'Winkelschleifer 125mm Akku', 'price'=>59.00, 'icon'=>'🔧','category'=>'Werkzeug','description'=>'Akkubetrieben, 18V, ohne Akku. Wenig genutzt.','stock'=>3],
['provider_id'=>2, 'title'=>'Werkzeugkoffer 108-teilig', 'price'=>55.00, 'icon'=>'🧰','category'=>'Werkzeug', 'description'=>'Kompletter Satz, Koffer leicht beschädigt, Werkzeug vollständig.','stock'=>1],
['provider_id'=>12,'title'=>'Niveau-Laser Selbstnivell.', 'price'=>70.00, 'icon'=>'🔧','category'=>'Werkzeug', 'description'=>'Kreuzlinienlaser, Stativ inkl. Restposten Ausstellung.','stock'=>3], ['provider_id'=>$pid(6), 'title'=>'Kreissäge 1400W', 'price'=>95.00, 'icon'=>'🪚','category'=>'Werkzeug','description'=>'185mm Sägeblatt, Parallelanschlag. Guter Zustand.','stock'=>1],
['provider_id'=>9, 'title'=>'Schubkarre 80L verzinkt', 'price'=>28.00, 'icon'=>'🛒','category'=>'Werkzeug', 'description'=>'Stahl, luftbereifter Reifen. 5 Stück.','stock'=>5], ['provider_id'=>$pid(2), 'title'=>'Kreissäge 1200W kompakt', 'price'=>78.00, 'icon'=>'🪚','category'=>'Werkzeug','description'=>'165mm Sägeblatt, leichtgängig. Wenig benutzt.','stock'=>2],
['provider_id'=>$pid(6), 'title'=>'Stichsäge Bosch PST', 'price'=>42.00, 'icon'=>'🔧','category'=>'Werkzeug','description'=>'650W, inkl. 3 Sägeblätter. Wenig genutzt.','stock'=>2],
['provider_id'=>$pid(12),'title'=>'Stichsäge Makita', 'price'=>49.00, 'icon'=>'🔧','category'=>'Werkzeug','description'=>'720W, variable Hubzahl, inkl. Koffer.','stock'=>1],
['provider_id'=>$pid(6), 'title'=>'Schlagbohrmaschine 750W', 'price'=>38.00, 'icon'=>'⚙️','category'=>'Werkzeug','description'=>'Kabelgebunden, 13mm Bohrfutter. Sehr guter Zustand.','stock'=>4],
['provider_id'=>$pid(9), 'title'=>'Schlagbohrmaschine 850W', 'price'=>45.00, 'icon'=>'⚙️','category'=>'Werkzeug','description'=>'Kabelgebunden, 16mm Bohrfutter, stärker. Neuwertig.','stock'=>2],
['provider_id'=>$pid(2), 'title'=>'Werkzeugkoffer 108-teilig', 'price'=>55.00, 'icon'=>'🧰','category'=>'Werkzeug','description'=>'Kompletter Satz, Koffer leicht beschädigt, Werkzeug vollständig.','stock'=>1],
['provider_id'=>$pid(6), 'title'=>'Werkzeugkoffer 94-teilig', 'price'=>48.00, 'icon'=>'🧰','category'=>'Werkzeug','description'=>'Kleinerer Satz, originalverpackt.','stock'=>4],
['provider_id'=>$pid(12),'title'=>'Niveau-Laser Selbstnivell.', 'price'=>70.00, 'icon'=>'🔧','category'=>'Werkzeug','description'=>'Kreuzlinienlaser, Stativ inkl. Restposten Ausstellung.','stock'=>3],
['provider_id'=>$pid(9), 'title'=>'Niveau-Laser Profi', 'price'=>95.00, 'icon'=>'🔧','category'=>'Werkzeug','description'=>'Kreuzlinienlaser mit Empfänger, größere Reichweite.','stock'=>1],
['provider_id'=>$pid(9), 'title'=>'Schubkarre 80L verzinkt', 'price'=>28.00, 'icon'=>'🛒','category'=>'Werkzeug','description'=>'Stahl, luftbereifter Reifen. 5 Stück.','stock'=>5],
['provider_id'=>$pid(2), 'title'=>'Schubkarre 100L verzinkt', 'price'=>34.00, 'icon'=>'🛒','category'=>'Werkzeug','description'=>'Größeres Volumen, doppelter Reifen. 3 Stück.','stock'=>3],
// Holz & Platten // Holz & Platten
['provider_id'=>3, 'title'=>'Zaunlatten 180cm', 'price'=>3.20, 'icon'=>'🪵','category'=>'Holz & Platten','description'=>'Kiefernholz, druckimprägniert. VB ab 50 Stück.','stock'=>320], ['provider_id'=>$pid(3), 'title'=>'Zaunlatten 180cm', 'price'=>3.20, 'icon'=>'🪵','category'=>'Holz & Platten','description'=>'Kiefernholz, druckimprägniert. VB ab 50 Stück.','stock'=>320],
['provider_id'=>3, 'title'=>'OSB-Platten 18mm', 'price'=>14.50, 'icon'=>'📦','category'=>'Holz & Platten','description'=>'250x125cm, Zuschnitt möglich. 40 Platten.','stock'=>40], ['provider_id'=>$pid(10),'title'=>'Zaunlatten 150cm', 'price'=>2.60, 'icon'=>'🪵','category'=>'Holz & Platten','description'=>'Kiefernholz, kürzere Variante. 200 Stück.','stock'=>200],
['provider_id'=>10,'title'=>'Douglasie Terrassendielen', 'price'=>8.90, 'icon'=>'🪵','category'=>'Holz & Platten','description'=>'4m lang, 145x21mm. 60 Stück, Restposten.','stock'=>60],
['provider_id'=>10,'title'=>'Spanplatten 16mm 260x60', 'price'=>11.00, 'icon'=>'📦','category'=>'Holz & Platten','description'=>'Melaminbeschichtet weiß. 25 Stück.','stock'=>25], ['provider_id'=>$pid(3), 'title'=>'OSB-Platten 18mm', 'price'=>14.50, 'icon'=>'📦','category'=>'Holz & Platten','description'=>'250x125cm, Zuschnitt möglich. 40 Platten.','stock'=>40],
['provider_id'=>10,'title'=>'Leimholzplatte Buche 80x40', 'price'=>24.00, 'icon'=>'🪵','category'=>'Holz & Platten','description'=>'18mm stark, beidseitig geschliffen. 8 Stück.','stock'=>8], ['provider_id'=>$pid(10),'title'=>'OSB-Platten 15mm', 'price'=>11.90, 'icon'=>'📦','category'=>'Holz & Platten','description'=>'250x125cm, dünnere Variante. 60 Platten.','stock'=>60],
['provider_id'=>3, 'title'=>'Dachlatten 3x5cm 4m', 'price'=>2.10, 'icon'=>'🪵','category'=>'Holz & Platten','description'=>'Nadelholz, gehobelt. 200 Stück.','stock'=>200],
['provider_id'=>12,'title'=>'Gipsfaserplatten 12.5mm', 'price'=>9.80, 'icon'=>'📦','category'=>'Holz & Platten','description'=>'120x60cm. Restposten 35 Platten.','stock'=>35], ['provider_id'=>$pid(10),'title'=>'Douglasie Terrassendielen', 'price'=>8.90, 'icon'=>'🪵','category'=>'Holz & Platten','description'=>'4m lang, 145x21mm. 60 Stück, Restposten.','stock'=>60],
['provider_id'=>$pid(3), 'title'=>'Douglasie Terrassendielen B-Ware', 'price'=>6.50, 'icon'=>'🪵','category'=>'Holz & Platten','description'=>'Leichte optische Mängel, sonst einwandfrei.','stock'=>40],
['provider_id'=>$pid(10),'title'=>'Spanplatten 16mm 260x60', 'price'=>11.00, 'icon'=>'📦','category'=>'Holz & Platten','description'=>'Melaminbeschichtet weiß. 25 Stück.','stock'=>25],
['provider_id'=>$pid(12),'title'=>'Spanplatten 19mm 260x60', 'price'=>13.50, 'icon'=>'📦','category'=>'Holz & Platten','description'=>'Stärkere Variante, melaminbeschichtet.','stock'=>15],
['provider_id'=>$pid(10),'title'=>'Leimholzplatte Buche 80x40', 'price'=>24.00, 'icon'=>'🪵','category'=>'Holz & Platten','description'=>'18mm stark, beidseitig geschliffen. 8 Stück.','stock'=>8],
['provider_id'=>$pid(3), 'title'=>'Leimholzplatte Eiche 80x40', 'price'=>32.00, 'icon'=>'🪵','category'=>'Holz & Platten','description'=>'19mm stark, hochwertiger.','stock'=>5],
['provider_id'=>$pid(3), 'title'=>'Dachlatten 3x5cm 4m', 'price'=>2.10, 'icon'=>'🪵','category'=>'Holz & Platten','description'=>'Nadelholz, gehobelt. 200 Stück.','stock'=>200],
['provider_id'=>$pid(10),'title'=>'Dachlatten 4x6cm 4m', 'price'=>2.85, 'icon'=>'🪵','category'=>'Holz & Platten','description'=>'Stärkeres Profil, sägerau. 150 Stück.','stock'=>150],
['provider_id'=>$pid(12),'title'=>'Gipsfaserplatten 12.5mm', 'price'=>9.80, 'icon'=>'📦','category'=>'Holz & Platten','description'=>'120x60cm. Restposten 35 Platten.','stock'=>35],
['provider_id'=>$pid(3), 'title'=>'Gipsfaserplatten 10mm', 'price'=>8.20, 'icon'=>'📦','category'=>'Holz & Platten','description'=>'120x60cm, dünnere Variante. 50 Platten.','stock'=>50],
// Farben & Lacke // Farben & Lacke
['provider_id'=>4, 'title'=>'Wandfarbe Weiß 10L', 'price'=>22.00, 'icon'=>'💧','category'=>'Farben & Lacke','description'=>'Dispersionsfarbe, innen. Restbestand aus Geschäftsauflösung.','stock'=>12], ['provider_id'=>$pid(4), 'title'=>'Wandfarbe Weiß 10L', 'price'=>22.00, 'icon'=>'💧','category'=>'Farben & Lacke','description'=>'Dispersionsfarbe, innen. Restbestand aus Geschäftsauflösung.','stock'=>12],
['provider_id'=>4, 'title'=>'Fassadenfarbe Grau 15L', 'price'=>48.00, 'icon'=>'🪣','category'=>'Farben & Lacke','description'=>'Wetterschutzfarbe, außen. 8 Eimer.','stock'=>8], ['provider_id'=>$pid(11),'title'=>'Wandfarbe Weiß 5L Premium', 'price'=>16.50, 'icon'=>'💧','category'=>'Farben & Lacke','description'=>'Hohe Deckkraft, kleineres Gebinde.','stock'=>20],
['provider_id'=>11,'title'=>'Holzlasur Nussbaum 5L', 'price'=>19.00, 'icon'=>'💧','category'=>'Farben & Lacke','description'=>'Seidenmatt, für innen und außen. 6 Stück.','stock'=>6],
['provider_id'=>11,'title'=>'Grundierung Universal 10L', 'price'=>27.00, 'icon'=>'🪣','category'=>'Farben & Lacke','description'=>'Tiefengrund für Innen- und Außenbereich. 4 Eimer.','stock'=>4], ['provider_id'=>$pid(4), 'title'=>'Fassadenfarbe Grau 15L', 'price'=>48.00, 'icon'=>'🪣','category'=>'Farben & Lacke','description'=>'Wetterschutzfarbe, außen. 8 Eimer.','stock'=>8],
['provider_id'=>11,'title'=>'Buntlack RAL 3000 750ml', 'price'=>8.50, 'icon'=>'💧','category'=>'Farben & Lacke','description'=>'Feuerrot, glänzend. Restposten 15 Dosen.','stock'=>15], ['provider_id'=>$pid(11),'title'=>'Fassadenfarbe Weiß 12L', 'price'=>39.00, 'icon'=>'🪣','category'=>'Farben & Lacke','description'=>'Wetterschutzfarbe, außen, kleineres Gebinde.','stock'=>10],
['provider_id'=>4, 'title'=>'Parkettlack seidenmatt 5L', 'price'=>34.00, 'icon'=>'💧','category'=>'Farben & Lacke','description'=>'Wasserbasis, geruchsarm. 3 Stück.','stock'=>3],
['provider_id'=>$pid(11),'title'=>'Holzlasur Nussbaum 5L', 'price'=>19.00, 'icon'=>'💧','category'=>'Farben & Lacke','description'=>'Seidenmatt, für innen und außen. 6 Stück.','stock'=>6],
['provider_id'=>$pid(4), 'title'=>'Holzlasur Eiche 5L', 'price'=>21.50, 'icon'=>'💧','category'=>'Farben & Lacke','description'=>'Seidenmatt, UV-beständig.','stock'=>4],
['provider_id'=>$pid(11),'title'=>'Grundierung Universal 10L', 'price'=>27.00, 'icon'=>'🪣','category'=>'Farben & Lacke','description'=>'Tiefengrund für Innen- und Außenbereich. 4 Eimer.','stock'=>4],
['provider_id'=>$pid(4), 'title'=>'Grundierung Universal 5L', 'price'=>15.50, 'icon'=>'🪣','category'=>'Farben & Lacke','description'=>'Kleineres Gebinde, gleiche Eigenschaften.','stock'=>9],
['provider_id'=>$pid(11),'title'=>'Buntlack RAL 3000 750ml', 'price'=>8.50, 'icon'=>'💧','category'=>'Farben & Lacke','description'=>'Feuerrot, glänzend. Restposten 15 Dosen.','stock'=>15],
['provider_id'=>$pid(4), 'title'=>'Buntlack RAL 5010 750ml', 'price'=>9.20, 'icon'=>'💧','category'=>'Farben & Lacke','description'=>'Enzianblau, glänzend.','stock'=>10],
['provider_id'=>$pid(4), 'title'=>'Parkettlack seidenmatt 5L', 'price'=>34.00, 'icon'=>'💧','category'=>'Farben & Lacke','description'=>'Wasserbasis, geruchsarm. 3 Stück.','stock'=>3],
['provider_id'=>$pid(11),'title'=>'Parkettlack glänzend 2.5L', 'price'=>19.00, 'icon'=>'💧','category'=>'Farben & Lacke','description'=>'Wasserbasis, kleineres Gebinde.','stock'=>6],
// Sanitär // Sanitär
['provider_id'=>1, 'title'=>'Badarmatur Chrom', 'price'=>45.00, 'icon'=>'🚿','category'=>'Sanitär', 'description'=>'Einhebelmischer, neuwertig, originalverpackt.','stock'=>4], ['provider_id'=>$pid(1), 'title'=>'Badarmatur Chrom', 'price'=>45.00, 'icon'=>'🚿','category'=>'Sanitär','description'=>'Einhebelmischer, neuwertig, originalverpackt.','stock'=>4],
['provider_id'=>7, 'title'=>'Duschkabine 80x80cm', 'price'=>220.00, 'icon'=>'🚿','category'=>'Sanitär', 'description'=>'Viertelkreis, Klarglas 5mm. Ausstellungsstück, minimale Kratzer.','stock'=>1], ['provider_id'=>$pid(7), 'title'=>'Badarmatur Matt Schwarz', 'price'=>59.00, 'icon'=>'🚿','category'=>'Sanitär','description'=>'Einhebelmischer, modernes Design.','stock'=>2],
['provider_id'=>7, 'title'=>'Waschbecken Keramik weiß', 'price'=>55.00, 'icon'=>'🪠','category'=>'Sanitär', 'description'=>'Unterbauwaschbecken 60cm. Neuwertig, keine Schäden.','stock'=>3],
['provider_id'=>7, 'title'=>'Heizkörper Typ 22 600x800', 'price'=>68.00, 'icon'=>'🌡️','category'=>'Sanitär', 'description'=>'Kompaktheizkörper, weiß. Originalverpackt. 5 Stück.','stock'=>5], ['provider_id'=>$pid(7), 'title'=>'Duschkabine 80x80cm', 'price'=>220.00, 'icon'=>'🚿','category'=>'Sanitär','description'=>'Viertelkreis, Klarglas 5mm. Ausstellungsstück, minimale Kratzer.','stock'=>1],
['provider_id'=>7, 'title'=>'WC-Sitz Softclose weiß', 'price'=>28.00, 'icon'=>'🪠','category'=>'Sanitär', 'description'=>'Absenkautomatik, abnehmbar. 4 Stück.','stock'=>4], ['provider_id'=>$pid(1), 'title'=>'Duschkabine 90x90cm', 'price'=>260.00, 'icon'=>'🚿','category'=>'Sanitär','description'=>'Viertelkreis, Klarglas 6mm. Neuwertig.','stock'=>1],
['provider_id'=>5, 'title'=>'Spültisch Edelstahl 2-Bec.', 'price'=>89.00, 'icon'=>'🚿','category'=>'Sanitär', 'description'=>'60x50cm, Unterbauspüle mit Ablauf. 2 Stück.','stock'=>2],
['provider_id'=>$pid(7), 'title'=>'Waschbecken Keramik weiß', 'price'=>55.00, 'icon'=>'🪠','category'=>'Sanitär','description'=>'Unterbauwaschbecken 60cm. Neuwertig, keine Schäden.','stock'=>3],
['provider_id'=>$pid(5), 'title'=>'Waschbecken Keramik oval', 'price'=>62.00, 'icon'=>'🪠','category'=>'Sanitär','description'=>'Aufsatzwaschbecken 50cm, ovale Form.','stock'=>2],
['provider_id'=>$pid(7), 'title'=>'Heizkörper Typ 22 600x800', 'price'=>68.00, 'icon'=>'🌡️','category'=>'Sanitär','description'=>'Kompaktheizkörper, weiß. Originalverpackt. 5 Stück.','stock'=>5],
['provider_id'=>$pid(1), 'title'=>'Heizkörper Typ 21 500x800', 'price'=>58.00, 'icon'=>'🌡️','category'=>'Sanitär','description'=>'Kompaktheizkörper, kleinere Bauform.','stock'=>3],
['provider_id'=>$pid(7), 'title'=>'WC-Sitz Softclose weiß', 'price'=>28.00, 'icon'=>'🪠','category'=>'Sanitär','description'=>'Absenkautomatik, abnehmbar. 4 Stück.','stock'=>4],
['provider_id'=>$pid(5), 'title'=>'WC-Sitz Softclose schwarz', 'price'=>32.00, 'icon'=>'🪠','category'=>'Sanitär','description'=>'Absenkautomatik, matt schwarz.','stock'=>2],
['provider_id'=>$pid(5), 'title'=>'Spültisch Edelstahl 2-Bec.', 'price'=>89.00, 'icon'=>'🚿','category'=>'Sanitär','description'=>'60x50cm, Unterbauspüle mit Ablauf. 2 Stück.','stock'=>2],
['provider_id'=>$pid(7), 'title'=>'Spültisch Edelstahl 1-Bec.', 'price'=>59.00, 'icon'=>'🚿','category'=>'Sanitär','description'=>'50x40cm, einfache Spüle mit Ablauf.','stock'=>3],
// Elektro // Elektro
['provider_id'=>2, 'title'=>'LED Feuchtraumleuchte 60cm', 'price'=>18.00, 'icon'=>'💡','category'=>'Elektro', 'description'=>'18W, IP65. Restposten 20 Stück.','stock'=>20], ['provider_id'=>$pid(2), 'title'=>'LED Feuchtraumleuchte 60cm', 'price'=>18.00, 'icon'=>'💡','category'=>'Elektro','description'=>'18W, IP65. Restposten 20 Stück.','stock'=>20],
['provider_id'=>8, 'title'=>'Unterputz-Steckdosen 10er', 'price'=>14.00, 'icon'=>'🔌','category'=>'Elektro', 'description'=>'Schuko, weiß, inkl. Rahmen. 10 Sets.','stock'=>10], ['provider_id'=>$pid(8), 'title'=>'LED Feuchtraumleuchte 120cm','price'=>26.00, 'icon'=>'💡','category'=>'Elektro','description'=>'36W, IP65, längere Variante.','stock'=>10],
['provider_id'=>8, 'title'=>'Kabelkanal 40x25mm 2m', 'price'=>3.80, 'icon'=>'📦','category'=>'Elektro', 'description'=>'Weiß, selbstklebend. 50 Stück.','stock'=>50],
['provider_id'=>8, 'title'=>'Sicherungsautomat B16 3-pol','price'=>22.00, 'icon'=>'⚡','category'=>'Elektro', 'description'=>'Leitungsschutzschalter, neuwertig. 8 Stück.','stock'=>8], ['provider_id'=>$pid(8), 'title'=>'Unterputz-Steckdosen 10er', 'price'=>14.00, 'icon'=>'🔌','category'=>'Elektro','description'=>'Schuko, weiß, inkl. Rahmen. 10 Sets.','stock'=>10],
['provider_id'=>8, 'title'=>'NYM-J 3x1.5mm² 50m', 'price'=>65.00, 'icon'=>'🔌','category'=>'Elektro', 'description'=>'Installationskabel, neu, originalverpackt. 3 Rollen.','stock'=>3], ['provider_id'=>$pid(9), 'title'=>'Unterputz-Steckdosen 5er', 'price'=>8.00, 'icon'=>'🔌','category'=>'Elektro','description'=>'Schuko, anthrazit, inkl. Rahmen.','stock'=>15],
['provider_id'=>9, 'title'=>'LED Baustrahler 50W', 'price'=>32.00, 'icon'=>'💡','category'=>'Elektro', 'description'=>'IP65, Schwenkarm, inkl. Stecker. 6 Stück.','stock'=>6],
['provider_id'=>5, 'title'=>'Verlängerungskabel 25m', 'price'=>29.00, 'icon'=>'🔌','category'=>'Elektro', 'description'=>'3x1.5mm², Schuko, orange. 4 Stück.','stock'=>4], ['provider_id'=>$pid(8), 'title'=>'Kabelkanal 40x25mm 2m', 'price'=>3.80, 'icon'=>'📦','category'=>'Elektro','description'=>'Weiß, selbstklebend. 50 Stück.','stock'=>50],
['provider_id'=>$pid(5), 'title'=>'Kabelkanal 60x40mm 2m', 'price'=>5.20, 'icon'=>'📦','category'=>'Elektro','description'=>'Größerer Querschnitt, weiß.','stock'=>30],
['provider_id'=>$pid(8), 'title'=>'Sicherungsautomat B16 3-pol','price'=>22.00, 'icon'=>'⚡','category'=>'Elektro','description'=>'Leitungsschutzschalter, neuwertig. 8 Stück.','stock'=>8],
['provider_id'=>$pid(9), 'title'=>'Sicherungsautomat B10 1-pol','price'=>9.50, 'icon'=>'⚡','category'=>'Elektro','description'=>'Leitungsschutzschalter, einpolig.','stock'=>12],
['provider_id'=>$pid(8), 'title'=>'NYM-J 3x1.5mm² 50m', 'price'=>65.00, 'icon'=>'🔌','category'=>'Elektro','description'=>'Installationskabel, neu, originalverpackt. 3 Rollen.','stock'=>3],
['provider_id'=>$pid(2), 'title'=>'NYM-J 5x1.5mm² 50m', 'price'=>89.00, 'icon'=>'🔌','category'=>'Elektro','description'=>'Installationskabel, 5-adrig.','stock'=>2],
['provider_id'=>$pid(9), 'title'=>'LED Baustrahler 50W', 'price'=>32.00, 'icon'=>'💡','category'=>'Elektro','description'=>'IP65, Schwenkarm, inkl. Stecker. 6 Stück.','stock'=>6],
['provider_id'=>$pid(8), 'title'=>'LED Baustrahler 100W', 'price'=>52.00, 'icon'=>'💡','category'=>'Elektro','description'=>'IP65, mit Stativ, stärkere Leistung.','stock'=>3],
['provider_id'=>$pid(5), 'title'=>'Verlängerungskabel 25m', 'price'=>29.00, 'icon'=>'🔌','category'=>'Elektro','description'=>'3x1.5mm², Schuko, orange. 4 Stück.','stock'=>4],
['provider_id'=>$pid(9), 'title'=>'Verlängerungskabel 10m', 'price'=>14.00, 'icon'=>'🔌','category'=>'Elektro','description'=>'3x1.5mm², Schuko, kürzere Variante.','stock'=>8],
]; ];
foreach ($products as $p) { foreach ($products as $p) {
DB::table('products')->insert(array_merge($p, ['created_at'=>now(),'updated_at'=>now()])); DB::table('products')->insert(array_merge($p, ['created_at'=>now(),'updated_at'=>now()]));
} }
DB::table('users')->insert([
'first_name' => 'Käufer',
'last_name' => 'Test',
'name' => 'Käufer Test',
'email' => 'kaeufer@test.de',
'password' => Hash::make('Passwort123!'),
'role' => 'buyer',
'email_verified_at' => now(),
'verification_code' => null,
'created_at' => now(),
'updated_at' => now(),
]);
} }
} }

View file

@ -26,3 +26,5 @@
Route::put('/seller/products/{id}', [SellerController::class, 'updateProduct']); Route::put('/seller/products/{id}', [SellerController::class, 'updateProduct']);
Route::delete('/seller/products/{id}', [SellerController::class, 'deleteProduct']); Route::delete('/seller/products/{id}', [SellerController::class, 'deleteProduct']);
Route::get('/seller/messages', [SellerController::class, 'getMessages']); Route::get('/seller/messages', [SellerController::class, 'getMessages']);
Route::get('/my-conversations', [MessageController::class, 'myConversations']);

View file

@ -17,11 +17,11 @@
<div class="dropdown-name">{{ user.first_name }} {{ user.last_name }}</div> <div class="dropdown-name">{{ user.first_name }} {{ user.last_name }}</div>
<div class="dropdown-role">{{ user.role === 'seller' ? 'Verkäufer' : 'Käufer' }}</div> <div class="dropdown-role">{{ user.role === 'seller' ? 'Verkäufer' : 'Käufer' }}</div>
<hr class="dropdown-divider" /> <hr class="dropdown-divider" />
<button @click="goTo('/profil')">👤 Mein Profil</button> <button @click="goTo('/chats')">💬 Meine Chats</button>
<button v-if="user.role === 'seller'" @click="goTo('/mein-shop')">🏪 Mein Shop</button> <button @click="goTo('/profil')">👤 Mein Profil</button>
<button @click="goTo('/bestellungen')">📦 Meine Bestellungen</button> <button v-if="user.role === 'seller'" @click="goTo('/mein-shop')">🏪 Mein Shop</button>
<hr class="dropdown-divider" /> <button @click="goTo('/bestellungen')">📦 Meine Bestellungen</button>
<button class="dropdown-logout" @click="logout">🚪 Abmelden</button> <button class="dropdown-logout" @click="logout">🚪 Abmelden</button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -78,6 +78,14 @@ async function authFetch(url, payload) {
return data return data
} }
export async function fetchMyConversations(role = 'buyer') {
const res = await fetch(`${BASE}/my-conversations?role=${role}`, {
headers: { Authorization: `Bearer ${localStorage.getItem('dilomarkt_token')}` }
})
if (!res.ok) throw new Error('Fehler beim Laden der Chats')
return res.json()
}
export const registerUser = (p) => authFetch(`${BASE}/auth/register`, p) export const registerUser = (p) => authFetch(`${BASE}/auth/register`, p)
export const verifyUser = (p) => authFetch(`${BASE}/auth/verify`, p) export const verifyUser = (p) => authFetch(`${BASE}/auth/verify`, p)
export const loginUser = (p) => authFetch(`${BASE}/auth/login`, p) export const loginUser = (p) => authFetch(`${BASE}/auth/login`, p)

View file

@ -28,6 +28,8 @@ const router = createRouter({
{ path: '/profil', name: 'profile', component: ProfileView }, { path: '/profil', name: 'profile', component: ProfileView },
{ path: '/bestellungen', name: 'orders', component: OrdersView }, { path: '/bestellungen', name: 'orders', component: OrdersView },
{ path: '/mein-shop', name: 'seller', component: SellerView }, { path: '/mein-shop', name: 'seller', component: SellerView },
{ path: '/chats', name: 'my-chats', component: () => import('../views/ChatOverviewView.vue') },
], ],
}) })

View file

@ -8,7 +8,7 @@
<div class="message-list" ref="listEl"> <div class="message-list" ref="listEl">
<div v-if="loading" style="color:#aaa;font-size:13px">Lädt...</div> <div v-if="loading" style="color:#aaa;font-size:13px">Lädt...</div>
<div v-for="m in messages" :key="m.id" :class="['msg', m.sender === 'buyer' ? 'msg-buyer' : 'msg-seller']"> <div v-for="m in messages" :key="m.id" :class="['msg', isOwnMessage(m) ? 'msg-own' : 'msg-other']">
<span>{{ m.body }}</span> <span>{{ m.body }}</span>
<small>{{ m.created_at }}</small> <small>{{ m.created_at }}</small>
</div> </div>
@ -43,11 +43,13 @@ const props = defineProps({
productId: [String, Number], productId: [String, Number],
productTitle: String, productTitle: String,
providerId: [String, Number], providerId: [String, Number],
buyerId: [String, Number],
}) })
defineEmits(['close']) defineEmits(['close'])
const { user } = useAuth() const { user } = useAuth()
const buyerId = () => user.value?.id ?? null const resolvedBuyerId = () => props.buyerId ?? user.value?.id ?? null
const messages = ref([]) const messages = ref([])
const draft = ref('') const draft = ref('')
@ -58,11 +60,15 @@ const listEl = ref(null)
let pollInterval = null let pollInterval = null
function isOwnMessage(m) {
return m.sender === (user.value?.role === 'seller' ? 'seller' : 'buyer')
}
async function loadMessages() { async function loadMessages() {
if (!buyerId()) return if (!resolvedBuyerId()) return
loading.value = true loading.value = true
try { try {
messages.value = await fetchMessages(props.productId, buyerId()) messages.value = await fetchMessages(props.productId, resolvedBuyerId())
await nextTick() await nextTick()
if (listEl.value) listEl.value.scrollTop = listEl.value.scrollHeight if (listEl.value) listEl.value.scrollTop = listEl.value.scrollHeight
} catch (e) { } catch (e) {
@ -72,6 +78,7 @@ async function loadMessages() {
} }
} }
async function send() { async function send() {
if (!draft.value.trim() || sending.value) return if (!draft.value.trim() || sending.value) return
sending.value = true sending.value = true
@ -80,8 +87,8 @@ async function send() {
await sendMessage({ await sendMessage({
product_id: props.productId, product_id: props.productId,
provider_id: props.providerId, provider_id: props.providerId,
buyer_id: buyerId(), buyer_id: resolvedBuyerId(),
sender: 'buyer', sender: user.value?.role === 'seller' ? 'seller' : 'buyer',
body: draft.value.trim(), body: draft.value.trim(),
}) })
draft.value = '' draft.value = ''
@ -93,6 +100,7 @@ async function send() {
} }
} }
watch(() => props.open, (val) => { watch(() => props.open, (val) => {
if (val) { if (val) {
loadMessages() loadMessages()
@ -102,7 +110,8 @@ watch(() => props.open, (val) => {
messages.value = [] messages.value = []
error.value = '' error.value = ''
} }
}) }, { immediate: true })
</script> </script>
<style scoped> <style scoped>
@ -120,6 +129,6 @@ watch(() => props.open, (val) => {
} }
.msg { display: flex; flex-direction: column; max-width: 80%; padding: 8px 12px; border-radius: 8px; font-size: 14px; } .msg { display: flex; flex-direction: column; max-width: 80%; padding: 8px 12px; border-radius: 8px; font-size: 14px; }
.msg small { font-size: 10px; color: #666; margin-top: 4px; } .msg small { font-size: 10px; color: #666; margin-top: 4px; }
.msg-buyer { align-self: flex-end; background: #1565c0; } .msg-own { align-self: flex-end; background: #1565c0; }
.msg-seller { align-self: flex-start; background: #2a2a2a; border: 1px solid #333; } .msg-other { align-self: flex-start; background: #2a2a2a; border: 1px solid #333; }
</style> </style>

View file

@ -0,0 +1,41 @@
<template>
<div style="max-width:700px;margin:0 auto;padding:20px">
<h2 style="margin-bottom:16px">Meine Chats</h2>
<div v-if="conversations.length === 0" style="color:#666">Noch keine Chats.</div>
<div v-for="c in conversations" :key="c.product_id + '-' + c.buyer_id"
@click="active = c"
style="background:#1e1e1e;border:1px solid #333;border-radius:8px;padding:12px;margin-bottom:8px;cursor:pointer">
<div style="display:flex;justify-content:space-between">
<strong>{{ c.product_title }}</strong>
<small style="color:#666">{{ formatDate(c.last_at) }}</small>
</div>
<div style="color:#aaa;font-size:13px">{{ c.provider_name }}</div>
<div style="color:#ccc;font-size:13px;margin-top:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
{{ c.last_message }}
</div>
</div>
<ChatModal v-if="active" open :product-id="active.product_id"
:product-title="active.product_title" :provider-id="active.provider_id"
:buyer-id="active.buyer_id" @close="active = null" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { fetchMyConversations } from '@/api.js'
import { useAuth } from '@/useAuth.js'
import ChatModal from './ChatModal.vue'
const { user } = useAuth()
const conversations = ref([])
const active = ref(null)
async function load() {
const role = user.value?.role === 'seller' ? 'seller' : 'buyer'
conversations.value = await fetchMyConversations(role)
}
function formatDate(d) { return d ? new Date(d).toLocaleString('de-DE') : '' }
onMounted(load)
</script>

View file

@ -14,10 +14,10 @@
<div class="comparison-box"> <div class="comparison-box">
<h3 style="margin-top:0">Preisvergleich</h3> <h3 style="margin-top:0">Preisvergleich</h3>
<div class="comparison-row active"> <div class="comparison-row active">
<span>{{ product.provider }}</span><strong>{{ product.price }} </strong> <span>{{ product.title }} {{ product.provider }}</span><strong>{{ product.price }} </strong>
</div> </div>
<div class="comparison-row" v-for="a in alternatives" :key="a.id"> <div class="comparison-row" v-for="a in alternatives" :key="a.id">
<span style="cursor:pointer;color:var(--primary-accent)" @click="router.push(`/produkt/${a.id}`)">{{ a.provider }}</span> <span style="cursor:pointer;color:var(--primary-accent)" @click="router.push(`/produkt/${a.id}`)">{{ a.title }} {{ a.provider }}</span>
<strong>{{ a.price }} </strong> <strong>{{ a.price }} </strong>
</div> </div>
<div v-if="!alternatives.length" style="color:#666;font-size:13px;padding-top:8px">Keine weiteren Angebote.</div> <div v-if="!alternatives.length" style="color:#666;font-size:13px;padding-top:8px">Keine weiteren Angebote.</div>
@ -50,26 +50,25 @@
<div v-else-if="loading" class="view-container" style="color:#aaa">Lädt...</div> <div v-else-if="loading" class="view-container" style="color:#aaa">Lädt...</div>
<div v-else class="view-container" style="color:#f44336">{{ error || 'Produkt nicht gefunden.' }}</div> <div v-else class="view-container" style="color:#f44336">{{ error || 'Produkt nicht gefunden.' }}</div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { fetchProduct } from '@/api.js' import { fetchProduct } from '@/api.js'
import ChatModal from '@/views/ChatModal.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const product = ref(null) const product = ref(null)
const alternatives = ref([]) const alternatives = ref([])
const loading = ref(true) const loading = ref(true)
const error = ref('')
import ChatModal from '@/views/ChatModal.vue'
const chatOpen = ref(false) const chatOpen = ref(false)
const error = ref('') async function loadProduct(id) {
loading.value = true
onMounted(async () => { error.value = ''
try { try {
const data = await fetchProduct(route.params.id) const data = await fetchProduct(id)
product.value = data.product product.value = data.product
alternatives.value = data.alternatives alternatives.value = data.alternatives
} catch (e) { } catch (e) {
@ -77,5 +76,14 @@ onMounted(async () => {
} finally { } finally {
loading.value = false loading.value = false
} }
}
onMounted(() => loadProduct(route.params.id))
watch(() => route.params.id, (newId) => {
if (newId) {
loadProduct(newId)
window.scrollTo({ top: 0, behavior: 'smooth' })
}
}) })
</script> </script>

View file

@ -113,6 +113,7 @@
Noch keine Produkte. Füge dein erstes Angebot hinzu. Noch keine Produkte. Füge dein erstes Angebot hinzu.
</div> </div>
<!-- Product rows --> <!-- Product rows -->
<div v-for="p in products" :key="p.id" class="product-row"> <div v-for="p in products" :key="p.id" class="product-row">
<span class="product-icon-cell">{{ p.icon }}</span> <span class="product-icon-cell">{{ p.icon }}</span>
@ -126,58 +127,8 @@
</div> </div>
</div> </div>
</section> </section>
<!-- MESSAGES SECTION -->
<section v-if="shop" class="seller-section">
<div class="section-head">
<h3>Nachrichten <span class="count-badge">{{ conversations.length }}</span></h3>
<button v-if="activeConv" class="btn-sm" @click="activeConv = null"> Zurück</button>
</div>
<!-- Conversation list -->
<template v-if="!activeConv">
<div v-if="conversations.length === 0" class="empty-hint">Noch keine Nachrichten von Käufern.</div>
<div v-for="c in conversations" :key="`${c.product_id}_${c.buyer_id}`"
class="conv-row" @click="openConversation(c)">
<div class="conv-info">
<strong>{{ c.buyer_name }}</strong>
<div class="meta">{{ c.product_title }}</div>
<div class="conv-preview">{{ c.last_message }}</div>
</div>
<span style="color:#555;font-size:20px"></span>
</div>
</template>
<!-- Thread view -->
<template v-else>
<div class="thread-header">
<strong>{{ activeConv.buyer_name }}</strong>
<div class="meta">{{ activeConv.product_title }}</div>
</div>
<div v-if="threadLoading" style="color:#aaa;padding:12px">Lädt...</div>
<div v-else class="thread-messages" ref="threadEl">
<div v-if="threadMessages.length === 0" class="empty-hint">Keine Nachrichten.</div>
<div v-for="m in threadMessages" :key="m.id"
:class="['bubble', m.sender === 'buyer' ? 'bubble-buyer' : 'bubble-seller']">
<span>{{ m.body }}</span>
<small>{{ new Date(m.created_at).toLocaleString('de-DE') }}</small>
</div>
</div>
<div style="display:flex;gap:8px;margin-top:12px">
<textarea v-model="replyDraft" rows="2" placeholder="Antwort schreiben…" class="reply-input"
@keydown.enter.exact.prevent="sendReply" />
<button class="btn-primary" @click="sendReply"
:disabled="replySending || !replyDraft.trim()"
style="align-self:flex-end;padding:10px 16px">
{{ replySending ? '…' : '➤' }}
</button>
</div>
<small style="color:#555;font-size:11px">Enter zum Senden</small>
</template>
</section>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, nextTick } from 'vue' import { ref, onMounted, nextTick } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@ -185,9 +136,7 @@ import { useAuth } from '@/useAuth.js'
import { import {
getSellerShop, saveSellerShop, getSellerShop, saveSellerShop,
addSellerProduct, updateSellerProduct, deleteSellerProduct, addSellerProduct, updateSellerProduct, deleteSellerProduct,
getSellerMessages,
} from '@/api.js' } from '@/api.js'
import { fetchMessages, sendMessage } from '@/api.js'
const CATEGORIES = ['Baumaterial', 'Werkzeug', 'Holz & Platten', 'Farben & Lacke', 'Sanitär', 'Elektro'] const CATEGORIES = ['Baumaterial', 'Werkzeug', 'Holz & Platten', 'Farben & Lacke', 'Sanitär', 'Elektro']
const CACHE_KEY = 'dilomarkt_seller_shop' const CACHE_KEY = 'dilomarkt_seller_shop'
@ -219,15 +168,6 @@ const showProductForm = ref(false)
const editingProductId = ref(null) const editingProductId = ref(null)
const productForm = ref({ title: '', price: '', category: 'Baumaterial', stock: 1, description: '', icon: '📦' }) const productForm = ref({ title: '', price: '', category: 'Baumaterial', stock: 1, description: '', icon: '📦' })
// Messages
const conversations = ref([])
const activeConv = ref(null)
const threadMessages = ref([])
const replyDraft = ref('')
const threadLoading = ref(false)
const replySending = ref(false)
const threadEl = ref(null)
onMounted(async () => { onMounted(async () => {
if (!user.value || user.value.role !== 'seller') { router.push('/'); return } if (!user.value || user.value.role !== 'seller') { router.push('/'); return }
await loadShop() await loadShop()
@ -242,7 +182,6 @@ async function loadShop() {
if (data.shop) populateShopForm(data.shop) if (data.shop) populateShopForm(data.shop)
// Persist to cache so next visit is instant // Persist to cache so next visit is instant
localStorage.setItem(CACHE_KEY, JSON.stringify({ shop: data.shop, products: data.products })) localStorage.setItem(CACHE_KEY, JSON.stringify({ shop: data.shop, products: data.products }))
if (data.shop) await loadConversations()
} finally { } finally {
loading.value = false loading.value = false
} }
@ -307,46 +246,6 @@ async function removeProduct(id) {
} catch (e) { showMsg(e.message, true) } } catch (e) { showMsg(e.message, true) }
} }
async function loadConversations() {
try {
const data = await getSellerMessages()
conversations.value = data.conversations
} catch { /* silent */ }
}
async function openConversation(c) {
activeConv.value = c
replyDraft.value = ''
threadLoading.value = true
try {
threadMessages.value = await fetchMessages(c.product_id, c.buyer_id)
await nextTick()
if (threadEl.value) threadEl.value.scrollTop = threadEl.value.scrollHeight
} finally {
threadLoading.value = false
}
}
async function sendReply() {
if (!replyDraft.value.trim() || replySending.value) return
replySending.value = true
try {
await sendMessage({
product_id: activeConv.value.product_id,
provider_id: shop.value.id,
buyer_id: activeConv.value.buyer_id,
sender: 'seller',
body: replyDraft.value.trim(),
})
replyDraft.value = ''
threadMessages.value = await fetchMessages(activeConv.value.product_id, activeConv.value.buyer_id)
await nextTick()
if (threadEl.value) threadEl.value.scrollTop = threadEl.value.scrollHeight
} finally {
replySending.value = false
}
}
function showMsg(text, isError) { function showMsg(text, isError) {
pageMsg.value = text pageMsg.value = text
msgIsError.value = isError msgIsError.value = isError