Files
hub/app/Http/Controllers/Api/TeamChatController.php
kelly 3e71c35c9e feat: add omnichannel inbox with team chat and buyer context
- 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
2025-12-17 02:15:23 -07:00

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