Some checks failed
ci/woodpecker/push/ci Pipeline failed
Backend:
- Add MarketplaceChatParticipant model for tracking thread participants
- Extend CrmThread with marketplace relationships (buyerBusiness, sellerBusiness, order)
- Add marketplace scopes to CrmThread for filtering B2B threads
- Create MarketplaceChatService for thread/message operations
- Create NewMarketplaceMessage broadcast event for real-time updates
- Create MarketplaceChatController API with thread/message endpoints
API Routes:
- GET /api/marketplace/chat/threads - List threads
- POST /api/marketplace/chat/threads - Create thread
- GET /api/marketplace/chat/threads/{id} - Get thread with messages
- POST /api/marketplace/chat/threads/{id}/messages - Send message
- POST /api/marketplace/chat/threads/{id}/read - Mark as read
- GET /api/marketplace/chat/unread-count - Get unread count
Frontend:
- Create marketplace-chat-widget component with Alpine.js
- Add floating chat button with unread badge
- Implement thread list and message views
- Add real-time message updates via Reverb/Echo
- Include widget in seller and buyer layouts
Broadcasting:
- Add marketplace-chat.{businessId} private channel
248 lines
8.2 KiB
PHP
248 lines
8.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Business;
|
|
use App\Models\Crm\CrmThread;
|
|
use App\Services\MarketplaceChatService;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
|
|
class MarketplaceChatController extends Controller
|
|
{
|
|
public function __construct(
|
|
protected MarketplaceChatService $chatService
|
|
) {}
|
|
|
|
/**
|
|
* List threads for the current business
|
|
*/
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$user = $request->user();
|
|
$businessId = $request->input('business_id');
|
|
|
|
if (! $businessId) {
|
|
return response()->json(['error' => 'business_id is required'], 400);
|
|
}
|
|
|
|
$business = Business::find($businessId);
|
|
|
|
if (! $business || ! $user->businesses->contains('id', $businessId)) {
|
|
return response()->json(['error' => 'Unauthorized'], 403);
|
|
}
|
|
|
|
$threads = $this->chatService->getThreadsForUser($user, $business);
|
|
|
|
return response()->json([
|
|
'threads' => $threads->map(fn ($thread) => $this->formatThread($thread, $business)),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get a single thread with messages
|
|
*/
|
|
public function show(Request $request, CrmThread $thread): JsonResponse
|
|
{
|
|
$user = $request->user();
|
|
|
|
if (! $this->chatService->canAccessThread($thread, $user)) {
|
|
return response()->json(['error' => 'Unauthorized'], 403);
|
|
}
|
|
|
|
$beforeId = $request->input('before_id');
|
|
$limit = min($request->input('limit', 50), 100);
|
|
|
|
$messages = $this->chatService->getMessages($thread, $limit, $beforeId);
|
|
|
|
// Mark as read
|
|
$this->chatService->markAsRead($thread, $user);
|
|
|
|
$business = $user->primaryBusiness();
|
|
|
|
return response()->json([
|
|
'thread' => $this->formatThread($thread, $business),
|
|
'messages' => $messages->map(fn ($msg) => $this->formatMessage($msg)),
|
|
'has_more' => $messages->count() === $limit,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Create a new thread or get existing one
|
|
*/
|
|
public function store(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'buyer_business_id' => 'required|integer|exists:businesses,id',
|
|
'seller_business_id' => 'required|integer|exists:businesses,id',
|
|
'order_id' => 'nullable|integer|exists:orders,id',
|
|
'initial_message' => 'nullable|string|max:5000',
|
|
]);
|
|
|
|
$user = $request->user();
|
|
$userBusinessIds = $user->businesses->pluck('id')->toArray();
|
|
|
|
// Verify user belongs to one of the businesses
|
|
if (! in_array($validated['buyer_business_id'], $userBusinessIds)
|
|
&& ! in_array($validated['seller_business_id'], $userBusinessIds)) {
|
|
return response()->json(['error' => 'Unauthorized'], 403);
|
|
}
|
|
|
|
$buyerBusiness = Business::findOrFail($validated['buyer_business_id']);
|
|
$sellerBusiness = Business::findOrFail($validated['seller_business_id']);
|
|
$order = isset($validated['order_id'])
|
|
? \App\Models\Order::find($validated['order_id'])
|
|
: null;
|
|
|
|
$thread = $this->chatService->getOrCreateThread($buyerBusiness, $sellerBusiness, $order);
|
|
|
|
// Send initial message if provided
|
|
if (! empty($validated['initial_message'])) {
|
|
$this->chatService->sendMessage($thread, $user, $validated['initial_message']);
|
|
}
|
|
|
|
$business = $user->primaryBusiness();
|
|
|
|
return response()->json([
|
|
'thread' => $this->formatThread($thread->fresh(), $business),
|
|
], 201);
|
|
}
|
|
|
|
/**
|
|
* Send a message in a thread
|
|
*/
|
|
public function sendMessage(Request $request, CrmThread $thread): JsonResponse
|
|
{
|
|
$user = $request->user();
|
|
|
|
if (! $this->chatService->canAccessThread($thread, $user)) {
|
|
return response()->json(['error' => 'Unauthorized'], 403);
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'body' => 'required|string|max:5000',
|
|
'attachments' => 'nullable|array',
|
|
'attachments.*.url' => 'required_with:attachments|string',
|
|
'attachments.*.name' => 'required_with:attachments|string',
|
|
'attachments.*.type' => 'nullable|string',
|
|
'attachments.*.size' => 'nullable|integer',
|
|
]);
|
|
|
|
$message = $this->chatService->sendMessage(
|
|
$thread,
|
|
$user,
|
|
$validated['body'],
|
|
$validated['attachments'] ?? []
|
|
);
|
|
|
|
return response()->json([
|
|
'message' => $this->formatMessage($message),
|
|
], 201);
|
|
}
|
|
|
|
/**
|
|
* Mark thread as read
|
|
*/
|
|
public function markAsRead(Request $request, CrmThread $thread): JsonResponse
|
|
{
|
|
$user = $request->user();
|
|
|
|
if (! $this->chatService->canAccessThread($thread, $user)) {
|
|
return response()->json(['error' => 'Unauthorized'], 403);
|
|
}
|
|
|
|
$this->chatService->markAsRead($thread, $user);
|
|
|
|
return response()->json(['success' => true]);
|
|
}
|
|
|
|
/**
|
|
* Get unread count for user
|
|
*/
|
|
public function unreadCount(Request $request): JsonResponse
|
|
{
|
|
$user = $request->user();
|
|
$businessId = $request->input('business_id');
|
|
|
|
if (! $businessId) {
|
|
return response()->json(['error' => 'business_id is required'], 400);
|
|
}
|
|
|
|
$business = Business::find($businessId);
|
|
|
|
if (! $business || ! $user->businesses->contains('id', $businessId)) {
|
|
return response()->json(['error' => 'Unauthorized'], 403);
|
|
}
|
|
|
|
$count = $this->chatService->getUnreadCount($user, $business);
|
|
|
|
return response()->json(['unread_count' => $count]);
|
|
}
|
|
|
|
/**
|
|
* Format thread for JSON response
|
|
*/
|
|
protected function formatThread(CrmThread $thread, ?Business $currentBusiness): array
|
|
{
|
|
$otherBusiness = $currentBusiness
|
|
? $this->chatService->getOtherBusiness($thread, $currentBusiness)
|
|
: null;
|
|
|
|
$lastMessage = $thread->messages->first();
|
|
|
|
return [
|
|
'id' => $thread->id,
|
|
'subject' => $thread->subject,
|
|
'status' => $thread->status,
|
|
'buyer_business' => $thread->buyerBusiness ? [
|
|
'id' => $thread->buyerBusiness->id,
|
|
'name' => $thread->buyerBusiness->name,
|
|
'slug' => $thread->buyerBusiness->slug,
|
|
] : null,
|
|
'seller_business' => $thread->sellerBusiness ? [
|
|
'id' => $thread->sellerBusiness->id,
|
|
'name' => $thread->sellerBusiness->name,
|
|
'slug' => $thread->sellerBusiness->slug,
|
|
] : null,
|
|
'other_business' => $otherBusiness ? [
|
|
'id' => $otherBusiness->id,
|
|
'name' => $otherBusiness->name,
|
|
'slug' => $otherBusiness->slug,
|
|
] : null,
|
|
'order' => $thread->order ? [
|
|
'id' => $thread->order->id,
|
|
'order_number' => $thread->order->order_number,
|
|
] : null,
|
|
'last_message' => $lastMessage ? [
|
|
'body' => \Str::limit($lastMessage->body, 100),
|
|
'sender_name' => $lastMessage->sender
|
|
? trim($lastMessage->sender->first_name.' '.$lastMessage->sender->last_name)
|
|
: 'Unknown',
|
|
'created_at' => $lastMessage->created_at->toIso8601String(),
|
|
] : null,
|
|
'last_message_at' => $thread->last_message_at?->toIso8601String(),
|
|
'created_at' => $thread->created_at->toIso8601String(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Format message for JSON response
|
|
*/
|
|
protected function formatMessage(mixed $message): array
|
|
{
|
|
return [
|
|
'id' => $message->id,
|
|
'thread_id' => $message->thread_id,
|
|
'body' => $message->body,
|
|
'sender_id' => $message->sender_id,
|
|
'sender_name' => $message->sender
|
|
? trim($message->sender->first_name.' '.$message->sender->last_name)
|
|
: 'Unknown',
|
|
'direction' => $message->direction,
|
|
'attachments' => $message->attachments,
|
|
'created_at' => $message->created_at->toIso8601String(),
|
|
];
|
|
}
|
|
}
|