chat support

This commit is contained in:
Abhishek Mali
2025-12-15 11:03:30 +05:30
parent 0a65d5f596
commit 1aad6b231e
31 changed files with 4670 additions and 35 deletions

View 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';
}
}

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

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

View File

@@ -11,7 +11,8 @@ use Tymon\JWTAuth\Exceptions\JWTException;
class JwtRefreshMiddleware
{
public function handle($request, Closure $next)
{
{
try {
JWTAuth::parseToken()->authenticate();
} catch (TokenExpiredException $e) {

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

View 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');
}
}

View 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');
}
}