- Redesign inbox with Chatwoot-style icon rail navigation - Add team chat infrastructure (TeamConversation, TeamMessage models) - Add buyer context tracking (current page, cart, recently viewed) - Show live buyer context in conversation sidebar - Add chat request status fields to CrmThread for buyer requests - Add broadcast channels for real-time team messaging
238 lines
8.0 KiB
PHP
238 lines
8.0 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\TeamConversation;
|
|
use App\Models\TeamMessage;
|
|
use App\Models\User;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
|
|
class TeamChatController extends Controller
|
|
{
|
|
/**
|
|
* Get all team conversations for current user
|
|
*/
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'business_id' => 'required|integer|exists:businesses,id',
|
|
]);
|
|
|
|
$user = $request->user();
|
|
|
|
// Verify user belongs to business
|
|
if (! $user->businesses()->where('businesses.id', $validated['business_id'])->exists()) {
|
|
return response()->json(['error' => 'Unauthorized'], 403);
|
|
}
|
|
|
|
$conversations = TeamConversation::forBusiness($validated['business_id'])
|
|
->forUser($user->id)
|
|
->with(['participants:id,first_name,last_name', 'messages' => fn ($q) => $q->latest()->limit(1)])
|
|
->orderByDesc('last_message_at')
|
|
->get()
|
|
->map(fn ($conv) => $this->formatConversation($conv, $user->id));
|
|
|
|
return response()->json(['conversations' => $conversations]);
|
|
}
|
|
|
|
/**
|
|
* Get or create a direct conversation with another user
|
|
*/
|
|
public function getOrCreateDirect(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'business_id' => 'required|integer|exists:businesses,id',
|
|
'user_id' => 'required|integer|exists:users,id',
|
|
]);
|
|
|
|
$user = $request->user();
|
|
|
|
// Verify current user belongs to business
|
|
if (! $user->businesses()->where('businesses.id', $validated['business_id'])->exists()) {
|
|
return response()->json(['error' => 'Unauthorized'], 403);
|
|
}
|
|
|
|
// Verify target user belongs to same business
|
|
$targetUser = User::find($validated['user_id']);
|
|
if (! $targetUser->businesses()->where('businesses.id', $validated['business_id'])->exists()) {
|
|
return response()->json(['error' => 'User not in business'], 400);
|
|
}
|
|
|
|
// Can't chat with yourself
|
|
if ($validated['user_id'] === $user->id) {
|
|
return response()->json(['error' => 'Cannot chat with yourself'], 400);
|
|
}
|
|
|
|
$conversation = TeamConversation::getOrCreateDirect(
|
|
$validated['business_id'],
|
|
$user->id,
|
|
$validated['user_id']
|
|
);
|
|
|
|
$conversation->load('participants:id,first_name,last_name');
|
|
|
|
return response()->json([
|
|
'conversation' => $this->formatConversation($conversation, $user->id),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get messages for a conversation
|
|
*/
|
|
public function messages(Request $request, int $conversationId): JsonResponse
|
|
{
|
|
$user = $request->user();
|
|
|
|
$conversation = TeamConversation::with('participants')
|
|
->findOrFail($conversationId);
|
|
|
|
// Verify user is participant
|
|
if (! $conversation->participants->contains('id', $user->id)) {
|
|
return response()->json(['error' => 'Unauthorized'], 403);
|
|
}
|
|
|
|
$messages = $conversation->messages()
|
|
->with('sender:id,first_name,last_name')
|
|
->orderBy('created_at')
|
|
->limit(100)
|
|
->get()
|
|
->map(fn ($msg) => $this->formatMessage($msg));
|
|
|
|
// Mark conversation as read
|
|
$conversation->markReadFor($user->id);
|
|
|
|
return response()->json(['messages' => $messages]);
|
|
}
|
|
|
|
/**
|
|
* Send a message to a conversation
|
|
*/
|
|
public function send(Request $request, int $conversationId): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'body' => 'required|string|max:10000',
|
|
'type' => 'sometimes|string|in:text,file,image',
|
|
'metadata' => 'sometimes|array',
|
|
]);
|
|
|
|
$user = $request->user();
|
|
|
|
$conversation = TeamConversation::with('participants')
|
|
->findOrFail($conversationId);
|
|
|
|
// Verify user is participant
|
|
if (! $conversation->participants->contains('id', $user->id)) {
|
|
return response()->json(['error' => 'Unauthorized'], 403);
|
|
}
|
|
|
|
$message = TeamMessage::create([
|
|
'conversation_id' => $conversationId,
|
|
'sender_id' => $user->id,
|
|
'body' => $validated['body'],
|
|
'type' => $validated['type'] ?? TeamMessage::TYPE_TEXT,
|
|
'metadata' => $validated['metadata'] ?? null,
|
|
'read_by' => [$user->id], // Sender has read it
|
|
]);
|
|
|
|
$message->load('sender:id,first_name,last_name');
|
|
|
|
return response()->json([
|
|
'message' => $this->formatMessage($message),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Mark conversation as read
|
|
*/
|
|
public function markRead(Request $request, int $conversationId): JsonResponse
|
|
{
|
|
$user = $request->user();
|
|
|
|
$conversation = TeamConversation::with('participants')
|
|
->findOrFail($conversationId);
|
|
|
|
// Verify user is participant
|
|
if (! $conversation->participants->contains('id', $user->id)) {
|
|
return response()->json(['error' => 'Unauthorized'], 403);
|
|
}
|
|
|
|
$conversation->markReadFor($user->id);
|
|
|
|
return response()->json(['success' => true]);
|
|
}
|
|
|
|
/**
|
|
* Get team members available for chat
|
|
*/
|
|
public function teamMembers(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'business_id' => 'required|integer|exists:businesses,id',
|
|
]);
|
|
|
|
$user = $request->user();
|
|
|
|
// Verify user belongs to business
|
|
if (! $user->businesses()->where('businesses.id', $validated['business_id'])->exists()) {
|
|
return response()->json(['error' => 'Unauthorized'], 403);
|
|
}
|
|
|
|
// Get all users in the business except current user
|
|
$members = User::whereHas('businesses', fn ($q) => $q->where('businesses.id', $validated['business_id']))
|
|
->where('id', '!=', $user->id)
|
|
->select('id', 'first_name', 'last_name')
|
|
->orderBy('first_name')
|
|
->get()
|
|
->map(fn ($u) => [
|
|
'id' => $u->id,
|
|
'name' => $u->name,
|
|
'initials' => strtoupper(substr($u->first_name ?? '', 0, 1).substr($u->last_name ?? '', 0, 1)),
|
|
]);
|
|
|
|
return response()->json(['members' => $members]);
|
|
}
|
|
|
|
/**
|
|
* Format conversation for API response
|
|
*/
|
|
private function formatConversation(TeamConversation $conversation, int $currentUserId): array
|
|
{
|
|
$other = $conversation->getOtherParticipant($currentUserId);
|
|
|
|
return [
|
|
'id' => $conversation->id,
|
|
'type' => $conversation->type,
|
|
'name' => $conversation->getDisplayName($currentUserId),
|
|
'other_user' => $other ? [
|
|
'id' => $other->id,
|
|
'name' => $other->name,
|
|
'initials' => strtoupper(substr($other->first_name ?? '', 0, 1).substr($other->last_name ?? '', 0, 1)),
|
|
] : null,
|
|
'last_message_preview' => $conversation->last_message_preview,
|
|
'last_message_at' => $conversation->last_message_at?->toIso8601String(),
|
|
'unread_count' => $conversation->getUnreadCountFor($currentUserId),
|
|
'is_pinned' => $conversation->participants->firstWhere('id', $currentUserId)?->pivot?->is_pinned ?? false,
|
|
'is_muted' => $conversation->participants->firstWhere('id', $currentUserId)?->pivot?->is_muted ?? false,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Format message for API response
|
|
*/
|
|
private function formatMessage(TeamMessage $message): array
|
|
{
|
|
return [
|
|
'id' => $message->id,
|
|
'sender_id' => $message->sender_id,
|
|
'sender_name' => $message->getSenderName(),
|
|
'sender_initials' => $message->getSenderInitials(),
|
|
'body' => $message->body,
|
|
'type' => $message->type,
|
|
'metadata' => $message->metadata,
|
|
'created_at' => $message->created_at->toIso8601String(),
|
|
];
|
|
}
|
|
}
|