Files
hub/app/Http/Controllers/Api/MarketplaceChatController.php
kelly 9c321b86c1
Some checks failed
ci/woodpecker/push/ci Pipeline failed
feat: implement B2B marketplace chat system
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
2025-12-15 16:14:31 -07:00

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(),
];
}
}