mirror of
https://github.com/serialexperiments0815/Dilomarkt---Digitaler-Lokalmarkt.git
synced 2026-07-12 15:32:19 +00:00
Erweiterung der Datenbank für tatsächliche Profile mit angeboten statt hardcoded. Chat funktionalität auf Käufer erweitert.
This commit is contained in:
parent
93fe6fa5ca
commit
37665ecf3e
13 changed files with 371 additions and 212 deletions
|
|
@ -1,8 +1,10 @@
|
|||
<?php
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Message;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class MessageController extends Controller
|
||||
{
|
||||
|
|
@ -26,4 +28,59 @@ public function store(Request $request)
|
|||
]));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -65,27 +65,36 @@ public function index(Request $request) {
|
|||
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')
|
||||
->select('products.*', 'providers.name as provider', 'providers.id as provider_id',
|
||||
'providers.address', 'providers.city', 'providers.zip',
|
||||
'providers.type as provider_type', 'providers.verified',
|
||||
'providers.since', 'providers.initials')
|
||||
->where('products.id', $id)
|
||||
->select(
|
||||
'products.id as id',
|
||||
'products.title', 'products.price', 'products.icon', 'products.category',
|
||||
'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);
|
||||
}
|
||||
|
||||
$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) // <- das fehlte
|
||||
->where('products.provider_id', '!=', $product->provider_id)
|
||||
->where('products.title', 'like', "%{$coreWord}%")
|
||||
->orderBy('products.price')
|
||||
->get();
|
||||
|
||||
return response()->json(['product' => $product, 'alternatives' => $alternatives]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,19 +8,18 @@ public function up(): void {
|
|||
Schema::create('providers', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('initials', 4);
|
||||
$table->string('address');
|
||||
$table->string('city');
|
||||
$table->string('zip', 10);
|
||||
$table->decimal('lat', 10, 7);
|
||||
$table->decimal('lng', 10, 7);
|
||||
$table->enum('type', ['Fachhandel', 'Baumarkt']);
|
||||
$table->unsignedSmallInteger('since');
|
||||
$table->boolean('verified')->default(false);
|
||||
$table->string('initials')->nullable();
|
||||
$table->string('address')->nullable();
|
||||
$table->string('city')->nullable();
|
||||
$table->string('zip')->nullable();
|
||||
$table->double('lat')->nullable();
|
||||
$table->double('lng')->nullable();
|
||||
$table->string('type')->nullable();
|
||||
$table->integer('since')->nullable();
|
||||
$table->boolean('verified')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void {
|
||||
Schema::dropIfExists('providers');
|
||||
}
|
||||
|
|
@ -9,15 +9,14 @@ public function up(): void {
|
|||
$table->id();
|
||||
$table->foreignId('provider_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('title');
|
||||
$table->decimal('price', 8, 2);
|
||||
$table->string('icon', 8);
|
||||
$table->string('category');
|
||||
$table->text('description');
|
||||
$table->unsignedInteger('stock')->default(0);
|
||||
$table->decimal('price', 10, 2);
|
||||
$table->string('icon')->nullable();
|
||||
$table->string('category')->nullable();
|
||||
$table->text('description')->nullable();
|
||||
$table->integer('stock')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void {
|
||||
Schema::dropIfExists('products');
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class DatabaseSeeder extends Seeder {
|
||||
public function run(): void {
|
||||
|
|
@ -10,9 +11,35 @@ public function run(): void {
|
|||
|
||||
DB::table('products')->truncate();
|
||||
DB::table('providers')->truncate();
|
||||
DB::table('users')->truncate();
|
||||
|
||||
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 = [
|
||||
['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],
|
||||
|
|
@ -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],
|
||||
];
|
||||
|
||||
foreach ($providers as $p) {
|
||||
DB::table('providers')->insert(array_merge($p, ['created_at'=>now(),'updated_at'=>now()]));
|
||||
$providerIds = [];
|
||||
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 = [
|
||||
// Baumaterial
|
||||
['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'=>1, 'title'=>'Betonmischer 140L', 'price'=>180.00, 'icon'=>'🏗️','category'=>'Baumaterial', 'description'=>'Elektrischer Betonmischer 140L, 500W. Einwandfreier Zustand.','stock'=>1],
|
||||
['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'=>9, 'title'=>'Ytong-Steine 20cm', 'price'=>3.50, 'icon'=>'🧱','category'=>'Baumaterial', 'description'=>'Porenbetonsteine, Restpalette. 40 Stück.','stock'=>40],
|
||||
['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'=>12,'title'=>'Kalksandstein KS-L', 'price'=>2.80, 'icon'=>'🧱','category'=>'Baumaterial', 'description'=>'20x20x10cm. Restposten 150 Stück.','stock'=>150],
|
||||
// Baumaterial — je Artikel min. 1 Alternative bei anderem Anbieter
|
||||
['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'=>$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'=>$pid(1), 'title'=>'Betonmischer 140L', 'price'=>180.00, 'icon'=>'🏗️','category'=>'Baumaterial','description'=>'Elektrischer Betonmischer 140L, 500W. Einwandfreier Zustand.','stock'=>1],
|
||||
['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'=>$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'=>$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
|
||||
['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'=>2, 'title'=>'Winkelschleifer 125mm', 'price'=>35.00, 'icon'=>'🔧','category'=>'Werkzeug', 'description'=>'900W, inkl. 5 Trennscheiben. Gebraucht, technisch einwandfrei.','stock'=>2],
|
||||
['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'=>6, 'title'=>'Schlagbohrmaschine 750W', 'price'=>38.00, 'icon'=>'⚙️','category'=>'Werkzeug', 'description'=>'Kabelgebunden, 13mm Bohrfutter. Sehr guter Zustand.','stock'=>4],
|
||||
['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'=>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'=>'Bohrmaschine 18V', 'price'=>64.00, 'icon'=>'⚙️','category'=>'Werkzeug','description'=>'Akkubohrmaschine 18V, 2 Akkus, Ladegerät inkl. Leichte Gebrauchsspuren.','stock'=>3],
|
||||
['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'=>$pid(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'=>'Winkelschleifer 125mm Akku', 'price'=>59.00, 'icon'=>'🔧','category'=>'Werkzeug','description'=>'Akkubetrieben, 18V, ohne Akku. Wenig genutzt.','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'=>$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
|
||||
['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'=>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'=>'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'=>10,'title'=>'Leimholzplatte Buche 80x40', 'price'=>24.00, 'icon'=>'🪵','category'=>'Holz & Platten','description'=>'18mm stark, beidseitig geschliffen. 8 Stück.','stock'=>8],
|
||||
['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(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(10),'title'=>'Zaunlatten 150cm', 'price'=>2.60, 'icon'=>'🪵','category'=>'Holz & Platten','description'=>'Kiefernholz, kürzere Variante. 200 Stück.','stock'=>200],
|
||||
|
||||
['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'=>$pid(10),'title'=>'OSB-Platten 15mm', 'price'=>11.90, 'icon'=>'📦','category'=>'Holz & Platten','description'=>'250x125cm, dünnere Variante. 60 Platten.','stock'=>60],
|
||||
|
||||
['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
|
||||
['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'=>4, 'title'=>'Fassadenfarbe Grau 15L', 'price'=>48.00, 'icon'=>'🪣','category'=>'Farben & Lacke','description'=>'Wetterschutzfarbe, außen. 8 Eimer.','stock'=>8],
|
||||
['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'=>11,'title'=>'Buntlack RAL 3000 750ml', 'price'=>8.50, 'icon'=>'💧','category'=>'Farben & Lacke','description'=>'Feuerrot, glänzend. Restposten 15 Dosen.','stock'=>15],
|
||||
['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(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(11),'title'=>'Wandfarbe Weiß 5L Premium', 'price'=>16.50, 'icon'=>'💧','category'=>'Farben & Lacke','description'=>'Hohe Deckkraft, kleineres Gebinde.','stock'=>20],
|
||||
|
||||
['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'=>$pid(11),'title'=>'Fassadenfarbe Weiß 12L', 'price'=>39.00, 'icon'=>'🪣','category'=>'Farben & Lacke','description'=>'Wetterschutzfarbe, außen, kleineres Gebinde.','stock'=>10],
|
||||
|
||||
['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
|
||||
['provider_id'=>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'=>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'=>7, 'title'=>'WC-Sitz Softclose weiß', 'price'=>28.00, 'icon'=>'🪠','category'=>'Sanitär', 'description'=>'Absenkautomatik, abnehmbar. 4 Stück.','stock'=>4],
|
||||
['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(1), 'title'=>'Badarmatur Chrom', 'price'=>45.00, 'icon'=>'🚿','category'=>'Sanitär','description'=>'Einhebelmischer, neuwertig, originalverpackt.','stock'=>4],
|
||||
['provider_id'=>$pid(7), 'title'=>'Badarmatur Matt Schwarz', 'price'=>59.00, 'icon'=>'🚿','category'=>'Sanitär','description'=>'Einhebelmischer, modernes Design.','stock'=>2],
|
||||
|
||||
['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'=>$pid(1), 'title'=>'Duschkabine 90x90cm', 'price'=>260.00, 'icon'=>'🚿','category'=>'Sanitär','description'=>'Viertelkreis, Klarglas 6mm. Neuwertig.','stock'=>1],
|
||||
|
||||
['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
|
||||
['provider_id'=>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'=>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'=>8, 'title'=>'NYM-J 3x1.5mm² 50m', 'price'=>65.00, 'icon'=>'🔌','category'=>'Elektro', 'description'=>'Installationskabel, neu, originalverpackt. 3 Rollen.','stock'=>3],
|
||||
['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(2), 'title'=>'LED Feuchtraumleuchte 60cm', 'price'=>18.00, 'icon'=>'💡','category'=>'Elektro','description'=>'18W, IP65. Restposten 20 Stück.','stock'=>20],
|
||||
['provider_id'=>$pid(8), 'title'=>'LED Feuchtraumleuchte 120cm','price'=>26.00, 'icon'=>'💡','category'=>'Elektro','description'=>'36W, IP65, längere Variante.','stock'=>10],
|
||||
|
||||
['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'=>$pid(9), 'title'=>'Unterputz-Steckdosen 5er', 'price'=>8.00, 'icon'=>'🔌','category'=>'Elektro','description'=>'Schuko, anthrazit, inkl. Rahmen.','stock'=>15],
|
||||
|
||||
['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) {
|
||||
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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -26,3 +26,5 @@
|
|||
Route::put('/seller/products/{id}', [SellerController::class, 'updateProduct']);
|
||||
Route::delete('/seller/products/{id}', [SellerController::class, 'deleteProduct']);
|
||||
Route::get('/seller/messages', [SellerController::class, 'getMessages']);
|
||||
|
||||
Route::get('/my-conversations', [MessageController::class, 'myConversations']);
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@
|
|||
<div class="dropdown-name">{{ user.first_name }} {{ user.last_name }}</div>
|
||||
<div class="dropdown-role">{{ user.role === 'seller' ? 'Verkäufer' : 'Käufer' }}</div>
|
||||
<hr class="dropdown-divider" />
|
||||
<button @click="goTo('/profil')">👤 Mein Profil</button>
|
||||
<button v-if="user.role === 'seller'" @click="goTo('/mein-shop')">🏪 Mein Shop</button>
|
||||
<button @click="goTo('/bestellungen')">📦 Meine Bestellungen</button>
|
||||
<hr class="dropdown-divider" />
|
||||
<button class="dropdown-logout" @click="logout">🚪 Abmelden</button>
|
||||
<button @click="goTo('/chats')">💬 Meine Chats</button>
|
||||
<button @click="goTo('/profil')">👤 Mein Profil</button>
|
||||
<button v-if="user.role === 'seller'" @click="goTo('/mein-shop')">🏪 Mein Shop</button>
|
||||
<button @click="goTo('/bestellungen')">📦 Meine Bestellungen</button>
|
||||
<button class="dropdown-logout" @click="logout">🚪 Abmelden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -78,6 +78,14 @@ async function authFetch(url, payload) {
|
|||
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 verifyUser = (p) => authFetch(`${BASE}/auth/verify`, p)
|
||||
export const loginUser = (p) => authFetch(`${BASE}/auth/login`, p)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ const router = createRouter({
|
|||
{ path: '/profil', name: 'profile', component: ProfileView },
|
||||
{ path: '/bestellungen', name: 'orders', component: OrdersView },
|
||||
{ path: '/mein-shop', name: 'seller', component: SellerView },
|
||||
{ path: '/chats', name: 'my-chats', component: () => import('../views/ChatOverviewView.vue') },
|
||||
|
||||
],
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
<div class="message-list" ref="listEl">
|
||||
<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>
|
||||
<small>{{ m.created_at }}</small>
|
||||
</div>
|
||||
|
|
@ -43,11 +43,13 @@ const props = defineProps({
|
|||
productId: [String, Number],
|
||||
productTitle: String,
|
||||
providerId: [String, Number],
|
||||
buyerId: [String, Number],
|
||||
})
|
||||
defineEmits(['close'])
|
||||
|
||||
const { user } = useAuth()
|
||||
const buyerId = () => user.value?.id ?? null
|
||||
const resolvedBuyerId = () => props.buyerId ?? user.value?.id ?? null
|
||||
|
||||
|
||||
const messages = ref([])
|
||||
const draft = ref('')
|
||||
|
|
@ -58,11 +60,15 @@ const listEl = ref(null)
|
|||
|
||||
let pollInterval = null
|
||||
|
||||
function isOwnMessage(m) {
|
||||
return m.sender === (user.value?.role === 'seller' ? 'seller' : 'buyer')
|
||||
}
|
||||
|
||||
async function loadMessages() {
|
||||
if (!buyerId()) return
|
||||
if (!resolvedBuyerId()) return
|
||||
loading.value = true
|
||||
try {
|
||||
messages.value = await fetchMessages(props.productId, buyerId())
|
||||
messages.value = await fetchMessages(props.productId, resolvedBuyerId())
|
||||
await nextTick()
|
||||
if (listEl.value) listEl.value.scrollTop = listEl.value.scrollHeight
|
||||
} catch (e) {
|
||||
|
|
@ -72,6 +78,7 @@ async function loadMessages() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
async function send() {
|
||||
if (!draft.value.trim() || sending.value) return
|
||||
sending.value = true
|
||||
|
|
@ -80,8 +87,8 @@ async function send() {
|
|||
await sendMessage({
|
||||
product_id: props.productId,
|
||||
provider_id: props.providerId,
|
||||
buyer_id: buyerId(),
|
||||
sender: 'buyer',
|
||||
buyer_id: resolvedBuyerId(),
|
||||
sender: user.value?.role === 'seller' ? 'seller' : 'buyer',
|
||||
body: draft.value.trim(),
|
||||
})
|
||||
draft.value = ''
|
||||
|
|
@ -93,6 +100,7 @@ async function send() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
watch(() => props.open, (val) => {
|
||||
if (val) {
|
||||
loadMessages()
|
||||
|
|
@ -102,7 +110,8 @@ watch(() => props.open, (val) => {
|
|||
messages.value = []
|
||||
error.value = ''
|
||||
}
|
||||
})
|
||||
}, { immediate: true })
|
||||
|
||||
</script>
|
||||
|
||||
<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 small { font-size: 10px; color: #666; margin-top: 4px; }
|
||||
.msg-buyer { align-self: flex-end; background: #1565c0; }
|
||||
.msg-seller { align-self: flex-start; background: #2a2a2a; border: 1px solid #333; }
|
||||
.msg-own { align-self: flex-end; background: #1565c0; }
|
||||
.msg-other { align-self: flex-start; background: #2a2a2a; border: 1px solid #333; }
|
||||
</style>
|
||||
41
Dilomarkt/src/views/ChatOverviewView.vue
Normal file
41
Dilomarkt/src/views/ChatOverviewView.vue
Normal 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>
|
||||
|
|
@ -14,10 +14,10 @@
|
|||
<div class="comparison-box">
|
||||
<h3 style="margin-top:0">Preisvergleich</h3>
|
||||
<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 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>
|
||||
</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 class="view-container" style="color:#f44336">{{ error || 'Produkt nicht gefunden.' }}</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { fetchProduct } from '@/api.js'
|
||||
import ChatModal from '@/views/ChatModal.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const product = ref(null)
|
||||
const alternatives = ref([])
|
||||
const loading = ref(true)
|
||||
|
||||
import ChatModal from '@/views/ChatModal.vue'
|
||||
const error = ref('')
|
||||
const chatOpen = ref(false)
|
||||
|
||||
const error = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
async function loadProduct(id) {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const data = await fetchProduct(route.params.id)
|
||||
const data = await fetchProduct(id)
|
||||
product.value = data.product
|
||||
alternatives.value = data.alternatives
|
||||
} catch (e) {
|
||||
|
|
@ -77,5 +76,14 @@ onMounted(async () => {
|
|||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => loadProduct(route.params.id))
|
||||
|
||||
watch(() => route.params.id, (newId) => {
|
||||
if (newId) {
|
||||
loadProduct(newId)
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -113,6 +113,7 @@
|
|||
Noch keine Produkte. Füge dein erstes Angebot hinzu.
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Product rows -->
|
||||
<div v-for="p in products" :key="p.id" class="product-row">
|
||||
<span class="product-icon-cell">{{ p.icon }}</span>
|
||||
|
|
@ -126,58 +127,8 @@
|
|||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
|
@ -185,9 +136,7 @@ import { useAuth } from '@/useAuth.js'
|
|||
import {
|
||||
getSellerShop, saveSellerShop,
|
||||
addSellerProduct, updateSellerProduct, deleteSellerProduct,
|
||||
getSellerMessages,
|
||||
} from '@/api.js'
|
||||
import { fetchMessages, sendMessage } from '@/api.js'
|
||||
|
||||
const CATEGORIES = ['Baumaterial', 'Werkzeug', 'Holz & Platten', 'Farben & Lacke', 'Sanitär', 'Elektro']
|
||||
const CACHE_KEY = 'dilomarkt_seller_shop'
|
||||
|
|
@ -219,15 +168,6 @@ const showProductForm = ref(false)
|
|||
const editingProductId = ref(null)
|
||||
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 () => {
|
||||
if (!user.value || user.value.role !== 'seller') { router.push('/'); return }
|
||||
await loadShop()
|
||||
|
|
@ -242,7 +182,6 @@ async function loadShop() {
|
|||
if (data.shop) populateShopForm(data.shop)
|
||||
// Persist to cache so next visit is instant
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify({ shop: data.shop, products: data.products }))
|
||||
if (data.shop) await loadConversations()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
|
@ -307,46 +246,6 @@ async function removeProduct(id) {
|
|||
} 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) {
|
||||
pageMsg.value = text
|
||||
msgIsError.value = isError
|
||||
|
|
|
|||
Loading…
Reference in a new issue