Some checks failed
ci/woodpecker/pr/ci Pipeline failed
Implements a Chatwoot-style unified inbox with real-time messaging
for sales reps to manage communications across all channels.
Features:
- Real-time messaging via Laravel Reverb WebSockets
- Sales rep filtering ("My Accounts Only") for assigned accounts
- Typing indicators showing when others are composing
- Agent status (online/away/busy) with heartbeat monitoring
- Email engagement sidebar (opens/clicks from marketing)
- Quick replies with variable substitution
- Presence awareness (who's online in team)
- Three-column layout: thread list, conversation, context sidebar
New files:
- Broadcasting events for real-time updates
- Unified inbox view with Alpine.js component
- 9 Blade partials for modular UI
Access via: Inbox → Conversations in sidebar
Route: /s/{business}/crm/inbox
104 lines
3.4 KiB
PHP
104 lines
3.4 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Events\CrmAgentStatusChanged;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\AgentStatus;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Validation\Rule;
|
|
|
|
class AgentStatusController extends Controller
|
|
{
|
|
public function update(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'business_id' => 'required|integer|exists:businesses,id',
|
|
'status' => ['required', Rule::in(array_keys(AgentStatus::statuses()))],
|
|
'status_message' => 'nullable|string|max:100',
|
|
]);
|
|
|
|
$user = $request->user();
|
|
|
|
// Verify user belongs to the business
|
|
if (! $user->businesses()->where('businesses.id', $validated['business_id'])->exists()) {
|
|
return response()->json(['error' => 'Unauthorized'], 403);
|
|
}
|
|
|
|
$agentStatus = AgentStatus::getOrCreate($user->id, $validated['business_id']);
|
|
$oldStatus = $agentStatus->status;
|
|
$agentStatus->setStatus($validated['status'], $validated['status_message'] ?? null);
|
|
|
|
// Broadcast status change if it changed
|
|
if ($oldStatus !== $validated['status']) {
|
|
broadcast(new CrmAgentStatusChanged($agentStatus->fresh()))->toOthers();
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'status' => $agentStatus->status,
|
|
'status_label' => AgentStatus::statuses()[$agentStatus->status],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Heartbeat to maintain online status
|
|
*/
|
|
public function heartbeat(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'business_id' => 'required|integer|exists:businesses,id',
|
|
]);
|
|
|
|
$user = $request->user();
|
|
|
|
// Verify user belongs to the business
|
|
if (! $user->businesses()->where('businesses.id', $validated['business_id'])->exists()) {
|
|
return response()->json(['error' => 'Unauthorized'], 403);
|
|
}
|
|
|
|
$agentStatus = AgentStatus::where('user_id', $user->id)
|
|
->where('business_id', $validated['business_id'])
|
|
->first();
|
|
|
|
if ($agentStatus) {
|
|
$agentStatus->updateLastSeen();
|
|
}
|
|
|
|
return response()->json(['success' => true]);
|
|
}
|
|
|
|
/**
|
|
* Get team members' statuses for a business
|
|
*/
|
|
public function team(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'business_id' => 'required|integer|exists:businesses,id',
|
|
]);
|
|
|
|
$user = $request->user();
|
|
|
|
// Verify user belongs to the business
|
|
if (! $user->businesses()->where('businesses.id', $validated['business_id'])->exists()) {
|
|
return response()->json(['error' => 'Unauthorized'], 403);
|
|
}
|
|
|
|
$statuses = AgentStatus::where('business_id', $validated['business_id'])
|
|
->where('status', '!=', AgentStatus::STATUS_OFFLINE)
|
|
->where('last_seen_at', '>=', now()->subMinutes(5))
|
|
->with('user:id,name')
|
|
->get()
|
|
->map(fn ($s) => [
|
|
'user_id' => $s->user_id,
|
|
'user_name' => $s->user?->name,
|
|
'status' => $s->status,
|
|
'status_message' => $s->status_message,
|
|
'last_seen_at' => $s->last_seen_at?->toIso8601String(),
|
|
]);
|
|
|
|
return response()->json(['team' => $statuses]);
|
|
}
|
|
}
|