mirror of
https://github.com/serialexperiments0815/Dilomarkt---Digitaler-Lokalmarkt.git
synced 2026-07-12 15:32:19 +00:00
Chat funktionalität und Datenbankerweitrerung
This commit is contained in:
parent
9c72c046ca
commit
adc70d4986
7 changed files with 214 additions and 1 deletions
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Message;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class MessageController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$messages = Message::where('product_id', $request->product_id)
|
||||||
|
->where('buyer_id', $request->buyer_id)
|
||||||
|
->orderBy('created_at')
|
||||||
|
->get();
|
||||||
|
return response()->json($messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$msg = Message::create($request->validate([
|
||||||
|
'product_id' => 'required|exists:products,id',
|
||||||
|
'provider_id' => 'required|exists:providers,id',
|
||||||
|
'buyer_id' => 'required|integer',
|
||||||
|
'sender' => 'required|in:buyer,seller',
|
||||||
|
'body' => 'required|string|max:1000',
|
||||||
|
]));
|
||||||
|
return response()->json($msg, 201);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Dilomarkt/dilomarkt-api/app/Models/Message.php
Normal file
9
Dilomarkt/dilomarkt-api/app/Models/Message.php
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Message extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = ['product_id', 'provider_id', 'buyer_id', 'sender', 'body'];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void {
|
||||||
|
Schema::create('messages', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('product_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('provider_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->unsignedBigInteger('buyer_id');
|
||||||
|
$table->enum('sender', ['buyer', 'seller']);
|
||||||
|
$table->text('body');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void {
|
||||||
|
Schema::dropIfExists('messages');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -2,8 +2,11 @@
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use App\Http\Controllers\ProductController;
|
use App\Http\Controllers\ProductController;
|
||||||
use App\Http\Controllers\ProviderController;
|
use App\Http\Controllers\ProviderController;
|
||||||
|
use App\Http\Controllers\MessageController;
|
||||||
|
|
||||||
Route::get('/products', [ProductController::class, 'index']);
|
Route::get('/products', [ProductController::class, 'index']);
|
||||||
Route::get('/products/{id}', [ProductController::class, 'show']);
|
Route::get('/products/{id}', [ProductController::class, 'show']);
|
||||||
Route::get('/providers', [ProviderController::class, 'index']);
|
Route::get('/providers', [ProviderController::class, 'index']);
|
||||||
Route::get('/providers/{id}', [ProviderController::class, 'show']);
|
Route::get('/providers/{id}', [ProviderController::class, 'show']);
|
||||||
|
Route::get('/messages', [MessageController::class, 'index']);
|
||||||
|
Route::post('/messages', [MessageController::class, 'store']);
|
||||||
|
|
|
||||||
|
|
@ -24,3 +24,19 @@ export async function fetchProvider(id) {
|
||||||
if (!res.ok) throw new Error('Anbieter nicht gefunden')
|
if (!res.ok) throw new Error('Anbieter nicht gefunden')
|
||||||
return res.json()
|
return res.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchMessages(productId, buyerId) {
|
||||||
|
const res = await fetch(`${BASE}/messages?product_id=${productId}&buyer_id=${buyerId}`)
|
||||||
|
if (!res.ok) throw new Error('Fehler beim Laden der Nachrichten')
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendMessage(payload) {
|
||||||
|
const res = await fetch(`${BASE}/messages`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error('Nachricht konnte nicht gesendet werden')
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
|
||||||
123
Dilomarkt/src/views/ChatModal.vue
Normal file
123
Dilomarkt/src/views/ChatModal.vue
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="open" class="modal-overlay" @click.self="$emit('close')">
|
||||||
|
<div class="modal-box">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||||
|
<h3 style="margin:0">Anfrage: {{ productTitle }}</h3>
|
||||||
|
<button @click="$emit('close')" style="background:none;border:none;color:#aaa;font-size:20px;cursor:pointer">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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']">
|
||||||
|
<span>{{ m.body }}</span>
|
||||||
|
<small>{{ m.created_at }}</small>
|
||||||
|
</div>
|
||||||
|
<div v-if="!loading && messages.length === 0" style="color:#666;font-size:13px;text-align:center;padding:20px">
|
||||||
|
Noch keine Nachrichten. Schreib dem Anbieter!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" style="color:#f44336;font-size:12px;margin-bottom:8px">{{ error }}</div>
|
||||||
|
|
||||||
|
<div style="display:flex;gap:8px;margin-top:12px">
|
||||||
|
<textarea v-model="draft" placeholder="Deine Nachricht..." rows="2"
|
||||||
|
style="flex:1;background:#2a2a2a;border:1px solid #333;color:#e0e0e0;border-radius:6px;padding:8px;resize:none;font-size:14px"
|
||||||
|
@keydown.enter.exact.prevent="send" />
|
||||||
|
<button class="btn-primary" @click="send" :disabled="sending || !draft.trim()"
|
||||||
|
style="align-self:flex-end;padding:10px 16px">
|
||||||
|
{{ sending ? '...' : '➤' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<small style="color:#555;font-size:11px">Enter zum Senden</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch, nextTick } from 'vue'
|
||||||
|
import { fetchMessages, sendMessage } from '@/api.js'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
open: Boolean,
|
||||||
|
productId: [String, Number],
|
||||||
|
productTitle: String,
|
||||||
|
providerId: [String, Number],
|
||||||
|
})
|
||||||
|
defineEmits(['close'])
|
||||||
|
|
||||||
|
// Hardcoded buyer_id=1 bis Auth fertig ist
|
||||||
|
const BUYER_ID = 1
|
||||||
|
|
||||||
|
const messages = ref([])
|
||||||
|
const draft = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const sending = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
const listEl = ref(null)
|
||||||
|
|
||||||
|
let pollInterval = null
|
||||||
|
|
||||||
|
async function loadMessages() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
messages.value = await fetchMessages(props.productId, BUYER_ID)
|
||||||
|
await nextTick()
|
||||||
|
if (listEl.value) listEl.value.scrollTop = listEl.value.scrollHeight
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e.message
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function send() {
|
||||||
|
if (!draft.value.trim() || sending.value) return
|
||||||
|
sending.value = true
|
||||||
|
error.value = ''
|
||||||
|
try {
|
||||||
|
await sendMessage({
|
||||||
|
product_id: props.productId,
|
||||||
|
provider_id: props.providerId,
|
||||||
|
buyer_id: BUYER_ID,
|
||||||
|
sender: 'buyer',
|
||||||
|
body: draft.value.trim(),
|
||||||
|
})
|
||||||
|
draft.value = ''
|
||||||
|
await loadMessages()
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e.message
|
||||||
|
} finally {
|
||||||
|
sending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.open, (val) => {
|
||||||
|
if (val) {
|
||||||
|
loadMessages()
|
||||||
|
pollInterval = setInterval(loadMessages, 5000)
|
||||||
|
} else {
|
||||||
|
clearInterval(pollInterval)
|
||||||
|
messages.value = []
|
||||||
|
error.value = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed; inset: 0; background: rgba(0,0,0,0.7);
|
||||||
|
display: flex; align-items: center; justify-content: center; z-index: 200;
|
||||||
|
}
|
||||||
|
.modal-box {
|
||||||
|
background: #1e1e1e; border: 1px solid #333; border-radius: 10px;
|
||||||
|
padding: 20px; width: 90%; max-width: 480px; display: flex; flex-direction: column;
|
||||||
|
}
|
||||||
|
.message-list {
|
||||||
|
min-height: 200px; max-height: 300px; overflow-y: auto;
|
||||||
|
display: flex; flex-direction: column; gap: 8px; padding: 4px 0;
|
||||||
|
}
|
||||||
|
.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; }
|
||||||
|
</style>
|
||||||
|
|
@ -24,6 +24,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-sidebar">
|
<div class="detail-sidebar">
|
||||||
|
<ChatModal
|
||||||
|
:open="chatOpen"
|
||||||
|
:product-id="product.id"
|
||||||
|
:product-title="product.title"
|
||||||
|
:provider-id="product.provider_id"
|
||||||
|
@close="chatOpen = false"
|
||||||
|
/>
|
||||||
<div class="seller-card" v-if="product">
|
<div class="seller-card" v-if="product">
|
||||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
|
||||||
<div class="seller-avatar">{{ product.initials }}</div>
|
<div class="seller-avatar">{{ product.initials }}</div>
|
||||||
|
|
@ -34,7 +41,7 @@
|
||||||
</div>
|
</div>
|
||||||
<p style="font-size:13px;color:#aaa">📦 {{ product.address }}, {{ product.city }}</p>
|
<p style="font-size:13px;color:#aaa">📦 {{ product.address }}, {{ product.city }}</p>
|
||||||
<div v-if="product.verified" style="font-size:12px;color:#4caf50;margin:8px 0">✅ Verifizierter Anbieter</div>
|
<div v-if="product.verified" style="font-size:12px;color:#4caf50;margin:8px 0">✅ Verifizierter Anbieter</div>
|
||||||
<button class="btn-action" @click="alert('Anfrage gesendet!')">✉️ Abholung anfragen</button>
|
<button class="btn-action" @click="chatOpen = true">✉️ Abholung anfragen</button>
|
||||||
<button class="btn-action" style="background:#333;margin-top:8px" @click="router.push(`/anbieter/${product.provider_id}`)">Alle Angebote ansehen</button>
|
<button class="btn-action" style="background:#333;margin-top:8px" @click="router.push(`/anbieter/${product.provider_id}`)">Alle Angebote ansehen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -54,6 +61,10 @@ 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)
|
||||||
|
|
||||||
|
import ChatModal from '@/views/ChatModal.vue'
|
||||||
|
const chatOpen = ref(false)
|
||||||
|
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue