chat support
This commit is contained in:
93
app/Events/NewChatMessage.php
Normal file
93
app/Events/NewChatMessage.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\ChatMessage;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class NewChatMessage implements ShouldBroadcastNow
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public $message;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(ChatMessage $message)
|
||||
{
|
||||
// Also load sender polymorphic relationship
|
||||
$message->load('sender');
|
||||
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* The channel the event should broadcast on.
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return [
|
||||
new PrivateChannel('ticket.' . $this->message->ticket_id),
|
||||
new PrivateChannel('admin.chat') // 👈 ADD THIS
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Data sent to frontend (Blade + Flutter)
|
||||
*/
|
||||
public function broadcastWith()
|
||||
{
|
||||
\Log::info("DEBUG: NewChatMessage broadcasting on channel ticket.".$this->message->ticket_id);
|
||||
|
||||
\Log::info("EVENT BROADCAST FIRED", [
|
||||
'channel' => 'ticket.'.$this->message->ticket_id,
|
||||
'sender_type' => $this->message->sender_type,
|
||||
'sender_id' => $this->message->sender_id,
|
||||
]);
|
||||
|
||||
|
||||
return [
|
||||
'id' => $this->message->id,
|
||||
'ticket_id' => $this->message->ticket_id,
|
||||
'sender_id' => $this->message->sender_id,
|
||||
'sender_type' => $this->message->sender_type,
|
||||
'message' => $this->message->message,
|
||||
'file_url' => $this->message->file_path
|
||||
? asset('storage/' . $this->message->file_path)
|
||||
: null,
|
||||
'file_type' => $this->message->file_type,
|
||||
'sender' => [
|
||||
'id' => $this->message->sender->id,
|
||||
'name' => $this->getSenderName(),
|
||||
'is_admin' => $this->message->sender_type === \App\Models\Admin::class,
|
||||
],
|
||||
'created_at' => $this->message->created_at->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to extract sender name
|
||||
*/
|
||||
private function getSenderName()
|
||||
{
|
||||
$sender = $this->message->sender;
|
||||
|
||||
// User has customer_name (in your app)
|
||||
if ($this->message->sender_type === \App\Models\User::class) {
|
||||
return $sender->customer_name ?? $sender->name ?? "User";
|
||||
}
|
||||
|
||||
// Admin model has ->name
|
||||
return $sender->name ?? "Admin";
|
||||
}
|
||||
|
||||
public function broadcastAs()
|
||||
{
|
||||
return 'NewChatMessage';
|
||||
}
|
||||
|
||||
}
|
||||
105
app/Http/Controllers/Admin/AdminChatController.php
Normal file
105
app/Http/Controllers/Admin/AdminChatController.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\SupportTicket;
|
||||
use App\Models\ChatMessage;
|
||||
use App\Events\NewChatMessage;
|
||||
|
||||
class AdminChatController extends Controller
|
||||
{
|
||||
/**
|
||||
* Page 1: List all active user chats
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$tickets = SupportTicket::with('user')
|
||||
->withCount([
|
||||
'messages as unread_count' => function ($q) {
|
||||
$q->where('sender_type', \App\Models\User::class)
|
||||
->where('read_by_admin', false);
|
||||
}
|
||||
])
|
||||
->orderBy('updated_at', 'desc')
|
||||
->get();
|
||||
|
||||
return view('admin.chat_support', compact('tickets'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Page 2: Open chat window for a specific user
|
||||
*/
|
||||
public function openChat($ticketId)
|
||||
{
|
||||
$ticket = SupportTicket::with('user')->findOrFail($ticketId);
|
||||
|
||||
// ✅ MARK USER MESSAGES AS READ FOR ADMIN
|
||||
ChatMessage::where('ticket_id', $ticketId)
|
||||
->where('sender_type', \App\Models\User::class)
|
||||
->where('read_by_admin', false)
|
||||
->update(['read_by_admin' => true]);
|
||||
|
||||
$messages = ChatMessage::where('ticket_id', $ticketId)
|
||||
->orderBy('created_at', 'asc')
|
||||
->with('sender')
|
||||
->get();
|
||||
|
||||
return view('admin.chat_window', compact('ticket', 'messages'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Admin sends a message to the user
|
||||
*/
|
||||
public function sendMessage(Request $request, $ticketId)
|
||||
{
|
||||
$request->validate([
|
||||
'message' => 'nullable|string',
|
||||
'file' => 'nullable|file|max:20480', // 20 MB
|
||||
]);
|
||||
|
||||
$ticket = SupportTicket::findOrFail($ticketId);
|
||||
$admin = auth('admin')->user();
|
||||
|
||||
$data = [
|
||||
'ticket_id' => $ticketId,
|
||||
'sender_id' => $admin->id,
|
||||
'sender_type' => \App\Models\Admin::class,
|
||||
'message' => $request->message,
|
||||
|
||||
'read_by_admin' => true,
|
||||
'read_by_user' => false,
|
||||
];
|
||||
|
||||
// File Upload
|
||||
if ($request->hasFile('file')) {
|
||||
$path = $request->file('file')->store('chat', 'public');
|
||||
$data['file_path'] = $path;
|
||||
$data['file_type'] = $request->file('file')->getMimeType();
|
||||
}
|
||||
|
||||
// Save message
|
||||
$message = ChatMessage::create($data);
|
||||
$message->load('sender');
|
||||
|
||||
\Log::info("DEBUG: ChatController sendMessage called", [
|
||||
'ticket_id' => $ticketId,
|
||||
'payload' => $request->all()
|
||||
]);
|
||||
|
||||
// Broadcast real-time
|
||||
broadcast(new NewChatMessage($message));
|
||||
|
||||
\Log::info("DEBUG: ChatController sendMessage called 79", [
|
||||
'ticket_id' => $ticketId,
|
||||
'payload' => $request->all()
|
||||
]);
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $message
|
||||
]);
|
||||
}
|
||||
}
|
||||
96
app/Http/Controllers/user/ChatController.php
Normal file
96
app/Http/Controllers/user/ChatController.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\SupportTicket;
|
||||
use App\Models\ChatMessage;
|
||||
use App\Events\NewChatMessage;
|
||||
|
||||
class ChatController extends Controller
|
||||
{
|
||||
/**
|
||||
* Start chat or return existing ticket for this user
|
||||
*/
|
||||
public function startChat()
|
||||
{
|
||||
// One chat ticket per user
|
||||
$ticket = SupportTicket::firstOrCreate([
|
||||
'user_id' => auth()->id(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'ticket' => $ticket
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all messages for this ticket
|
||||
*/
|
||||
public function getMessages($ticketId)
|
||||
{
|
||||
// Ensure this ticket belongs to the logged-in user
|
||||
$ticket = SupportTicket::where('id', $ticketId)
|
||||
->where('user_id', auth()->id())
|
||||
->firstOrFail();
|
||||
|
||||
$messages = ChatMessage::where('ticket_id', $ticketId)
|
||||
->orderBy('created_at', 'asc')
|
||||
->with('sender')
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'messages' => $messages
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send text or file message from user → admin/staff
|
||||
*/
|
||||
public function sendMessage(Request $request, $ticketId)
|
||||
{
|
||||
$request->validate([
|
||||
'message' => 'nullable|string',
|
||||
'file' => 'nullable|file|max:20480', // 20MB limit
|
||||
]);
|
||||
|
||||
// Validate ticket ownership
|
||||
$ticket = SupportTicket::where('id', $ticketId)
|
||||
->where('user_id', auth()->id())
|
||||
->firstOrFail();
|
||||
|
||||
$data = [
|
||||
'ticket_id' => $ticketId,
|
||||
'sender_id' => auth()->id(),
|
||||
'sender_type' => \App\Models\User::class,
|
||||
'message' => $request->message,
|
||||
|
||||
'read_by_admin' => false,
|
||||
'read_by_user' => true,
|
||||
];
|
||||
|
||||
// Handle file upload
|
||||
if ($request->hasFile('file')) {
|
||||
$path = $request->file('file')->store('chat', 'public');
|
||||
$data['file_path'] = $path;
|
||||
$data['file_type'] = $request->file('file')->getMimeType();
|
||||
}
|
||||
|
||||
// Save message
|
||||
$message = ChatMessage::create($data);
|
||||
|
||||
// Load sender info for broadcast
|
||||
$message->load('sender');
|
||||
|
||||
// Fire real-time event
|
||||
broadcast(new NewChatMessage($message));
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $message
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,8 @@ use Tymon\JWTAuth\Exceptions\JWTException;
|
||||
class JwtRefreshMiddleware
|
||||
{
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
{
|
||||
|
||||
try {
|
||||
JWTAuth::parseToken()->authenticate();
|
||||
} catch (TokenExpiredException $e) {
|
||||
|
||||
39
app/Models/ChatMessage.php
Normal file
39
app/Models/ChatMessage.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class ChatMessage extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'ticket_id',
|
||||
'sender_id',
|
||||
'sender_type',
|
||||
'message',
|
||||
'file_path',
|
||||
'file_type',
|
||||
'read_by_admin',
|
||||
'read_by_user',
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* The ticket this message belongs to.
|
||||
*/
|
||||
public function ticket()
|
||||
{
|
||||
return $this->belongsTo(SupportTicket::class, 'ticket_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Polymorphic sender (User or Admin)
|
||||
*/
|
||||
public function sender()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
||||
32
app/Models/SupportTicket.php
Normal file
32
app/Models/SupportTicket.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class SupportTicket extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'status',
|
||||
];
|
||||
|
||||
/**
|
||||
* The user (customer) who owns this ticket.
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* All chat messages for this ticket.
|
||||
*/
|
||||
public function messages()
|
||||
{
|
||||
return $this->hasMany(ChatMessage::class, 'ticket_id')->orderBy('created_at', 'asc');
|
||||
}
|
||||
}
|
||||
22
app/Providers/BroadcastServiceProvider.php
Normal file
22
app/Providers/BroadcastServiceProvider.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class BroadcastServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
Broadcast::routes([
|
||||
'middleware' => ['web'],
|
||||
]);
|
||||
|
||||
// 👇 FORCE admin guard for broadcasting
|
||||
Auth::shouldUse('admin');
|
||||
|
||||
require base_path('routes/channels.php');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user