feat: add chat settings UI with agent status and quick replies
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Some checks failed
ci/woodpecker/push/ci Pipeline failed
- Add AgentStatus model for tracking user availability (online/away/busy/offline) - Add ChatQuickReply model for pre-written chat responses - Add agent status toggle to seller account dropdown menu - Add quick replies management page under CRM settings - Create migration for chat_quick_replies, chat_attachments, agent_statuses tables - Add API endpoint for updating agent status
This commit is contained in:
37
app/Http/Controllers/Api/AgentStatusController.php
Normal file
37
app/Http/Controllers/Api/AgentStatusController.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
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']);
|
||||||
|
$agentStatus->setStatus($validated['status'], $validated['status_message'] ?? null);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'status' => $agentStatus->status,
|
||||||
|
'status_label' => AgentStatus::statuses()[$agentStatus->status],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Seller\Crm;
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Business;
|
use App\Models\Business;
|
||||||
|
use App\Models\ChatQuickReply;
|
||||||
use App\Models\Crm\CrmChannel;
|
use App\Models\Crm\CrmChannel;
|
||||||
use App\Models\Crm\CrmMessageTemplate;
|
use App\Models\Crm\CrmMessageTemplate;
|
||||||
use App\Models\Crm\CrmPipeline;
|
use App\Models\Crm\CrmPipeline;
|
||||||
@@ -649,4 +650,81 @@ class CrmSettingsController extends Controller
|
|||||||
|
|
||||||
return back()->with('success', 'Role deleted.');
|
return back()->with('success', 'Role deleted.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quick replies list
|
||||||
|
*/
|
||||||
|
public function quickReplies(Request $request, Business $business)
|
||||||
|
{
|
||||||
|
$quickReplies = ChatQuickReply::where('business_id', $business->id)
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->orderBy('label')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$categories = $quickReplies->pluck('category')->filter()->unique()->values();
|
||||||
|
|
||||||
|
return view('seller.crm.settings.quick-replies.index', [
|
||||||
|
'business' => $business,
|
||||||
|
'quickReplies' => $quickReplies,
|
||||||
|
'categories' => $categories,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store new quick reply
|
||||||
|
*/
|
||||||
|
public function storeQuickReply(Request $request, Business $business)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'label' => 'required|string|max:100',
|
||||||
|
'message' => 'required|string|max:2000',
|
||||||
|
'category' => 'nullable|string|max:50',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$validated['business_id'] = $business->id;
|
||||||
|
$validated['is_active'] = $request->boolean('is_active', true);
|
||||||
|
$validated['sort_order'] = ChatQuickReply::where('business_id', $business->id)->max('sort_order') + 1;
|
||||||
|
|
||||||
|
ChatQuickReply::create($validated);
|
||||||
|
|
||||||
|
return back()->with('success', 'Quick reply created.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update quick reply
|
||||||
|
*/
|
||||||
|
public function updateQuickReply(Request $request, Business $business, ChatQuickReply $quickReply)
|
||||||
|
{
|
||||||
|
if ($quickReply->business_id !== $business->id) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'label' => 'required|string|max:100',
|
||||||
|
'message' => 'required|string|max:2000',
|
||||||
|
'category' => 'nullable|string|max:50',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$validated['is_active'] = $request->boolean('is_active', true);
|
||||||
|
|
||||||
|
$quickReply->update($validated);
|
||||||
|
|
||||||
|
return back()->with('success', 'Quick reply updated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete quick reply
|
||||||
|
*/
|
||||||
|
public function destroyQuickReply(Request $request, Business $business, ChatQuickReply $quickReply)
|
||||||
|
{
|
||||||
|
if ($quickReply->business_id !== $business->id) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$quickReply->delete();
|
||||||
|
|
||||||
|
return back()->with('success', 'Quick reply deleted.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
90
app/Models/AgentStatus.php
Normal file
90
app/Models/AgentStatus.php
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class AgentStatus extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'business_id',
|
||||||
|
'status',
|
||||||
|
'status_message',
|
||||||
|
'last_seen_at',
|
||||||
|
'status_changed_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'last_seen_at' => 'datetime',
|
||||||
|
'status_changed_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
public const STATUS_ONLINE = 'online';
|
||||||
|
public const STATUS_AWAY = 'away';
|
||||||
|
public const STATUS_BUSY = 'busy';
|
||||||
|
public const STATUS_OFFLINE = 'offline';
|
||||||
|
|
||||||
|
public static function statuses(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::STATUS_ONLINE => 'Online',
|
||||||
|
self::STATUS_AWAY => 'Away',
|
||||||
|
self::STATUS_BUSY => 'Busy',
|
||||||
|
self::STATUS_OFFLINE => 'Offline',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function statusColors(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::STATUS_ONLINE => 'success',
|
||||||
|
self::STATUS_AWAY => 'warning',
|
||||||
|
self::STATUS_BUSY => 'error',
|
||||||
|
self::STATUS_OFFLINE => 'ghost',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function business(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Business::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getOrCreate(int $userId, int $businessId): self
|
||||||
|
{
|
||||||
|
return self::firstOrCreate(
|
||||||
|
['user_id' => $userId, 'business_id' => $businessId],
|
||||||
|
['status' => self::STATUS_OFFLINE, 'status_changed_at' => now()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStatus(string $status, ?string $message = null): self
|
||||||
|
{
|
||||||
|
$this->update([
|
||||||
|
'status' => $status,
|
||||||
|
'status_message' => $message,
|
||||||
|
'status_changed_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isOnline(): bool
|
||||||
|
{
|
||||||
|
return $this->status === self::STATUS_ONLINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatusColor(): string
|
||||||
|
{
|
||||||
|
return self::statusColors()[$this->status] ?? 'ghost';
|
||||||
|
}
|
||||||
|
}
|
||||||
50
app/Models/ChatQuickReply.php
Normal file
50
app/Models/ChatQuickReply.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class ChatQuickReply extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'chat_quick_replies';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'business_id',
|
||||||
|
'label',
|
||||||
|
'message',
|
||||||
|
'category',
|
||||||
|
'usage_count',
|
||||||
|
'is_active',
|
||||||
|
'sort_order',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'usage_count' => 'integer',
|
||||||
|
'sort_order' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function business(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Business::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeActive($query)
|
||||||
|
{
|
||||||
|
return $query->where('is_active', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeByCategory($query, string $category)
|
||||||
|
{
|
||||||
|
return $query->where('category', $category);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function incrementUsage(): void
|
||||||
|
{
|
||||||
|
$this->increment('usage_count');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Add rating fields to crm_threads (skip if already exist)
|
||||||
|
if (!Schema::hasColumn('crm_threads', 'rating')) {
|
||||||
|
Schema::table('crm_threads', function (Blueprint $table) {
|
||||||
|
$table->unsignedTinyInteger('rating')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('crm_threads', 'rating_comment')) {
|
||||||
|
Schema::table('crm_threads', function (Blueprint $table) {
|
||||||
|
$table->text('rating_comment')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('crm_threads', 'rated_at')) {
|
||||||
|
Schema::table('crm_threads', function (Blueprint $table) {
|
||||||
|
$table->timestamp('rated_at')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('crm_threads', 'ai_summary')) {
|
||||||
|
Schema::table('crm_threads', function (Blueprint $table) {
|
||||||
|
$table->text('ai_summary')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('crm_threads', 'summary_generated_at')) {
|
||||||
|
Schema::table('crm_threads', function (Blueprint $table) {
|
||||||
|
$table->timestamp('summary_generated_at')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create chat_attachments table
|
||||||
|
if (!Schema::hasTable('chat_attachments')) {
|
||||||
|
Schema::create('chat_attachments', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('message_id')->constrained('crm_channel_messages')->cascadeOnDelete();
|
||||||
|
$table->foreignId('business_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->string('filename');
|
||||||
|
$table->string('original_filename');
|
||||||
|
$table->string('mime_type');
|
||||||
|
$table->unsignedBigInteger('size');
|
||||||
|
$table->string('path');
|
||||||
|
$table->string('disk')->default('minio');
|
||||||
|
$table->json('metadata')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['message_id']);
|
||||||
|
$table->index(['business_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create quick_replies table
|
||||||
|
if (!Schema::hasTable('chat_quick_replies')) {
|
||||||
|
Schema::create('chat_quick_replies', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('business_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->string('label');
|
||||||
|
$table->text('message');
|
||||||
|
$table->string('category')->nullable();
|
||||||
|
$table->unsignedInteger('usage_count')->default(0);
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->unsignedInteger('sort_order')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['business_id', 'is_active']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create agent_statuses table for tracking availability
|
||||||
|
if (!Schema::hasTable('agent_statuses')) {
|
||||||
|
Schema::create('agent_statuses', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('business_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->string('status')->default('offline'); // online, away, busy, offline
|
||||||
|
$table->string('status_message')->nullable();
|
||||||
|
$table->timestamp('last_seen_at')->nullable();
|
||||||
|
$table->timestamp('status_changed_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['user_id', 'business_id']);
|
||||||
|
$table->index(['business_id', 'status']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('crm_threads', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['rating', 'rating_comment', 'rated_at', 'ai_summary', 'summary_generated_at']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::dropIfExists('chat_attachments');
|
||||||
|
Schema::dropIfExists('chat_quick_replies');
|
||||||
|
Schema::dropIfExists('agent_statuses');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -18,6 +18,17 @@
|
|||||||
$isOwner = $business && $business->owner_user_id === $user->id;
|
$isOwner = $business && $business->owner_user_id === $user->id;
|
||||||
$isSuperAdmin = $user->user_type === 'admin';
|
$isSuperAdmin = $user->user_type === 'admin';
|
||||||
$canManageSettings = $isOwner || $isSuperAdmin;
|
$canManageSettings = $isOwner || $isSuperAdmin;
|
||||||
|
|
||||||
|
// Get agent status for chat availability (sellers only)
|
||||||
|
$agentStatus = null;
|
||||||
|
if ($business && $user->user_type === 'seller') {
|
||||||
|
$agentStatus = \App\Models\AgentStatus::where('user_id', $user->id)
|
||||||
|
->where('business_id', $business->id)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
$currentStatus = $agentStatus?->status ?? 'offline';
|
||||||
|
$statusColors = \App\Models\AgentStatus::statusColors();
|
||||||
|
$statuses = \App\Models\AgentStatus::statuses();
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div x-data="{ accountOpen: false }" class="relative">
|
<div x-data="{ accountOpen: false }" class="relative">
|
||||||
@@ -80,6 +91,88 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
{{-- Chat Availability (Sellers only) --}}
|
||||||
|
@if($business && $user->user_type === 'seller')
|
||||||
|
<li class="pt-2 mt-2 border-t border-base-200">
|
||||||
|
<span class="px-3 text-xs font-semibold text-base-content/40 uppercase tracking-wider">Chat Availability</span>
|
||||||
|
</li>
|
||||||
|
<li x-data="{
|
||||||
|
status: '{{ $currentStatus }}',
|
||||||
|
updating: false,
|
||||||
|
async setStatus(newStatus) {
|
||||||
|
if (this.updating || this.status === newStatus) return;
|
||||||
|
this.updating = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch('{{ route('api.agent-status.update') }}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ status: newStatus, business_id: {{ $business->id }} })
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
this.status = newStatus;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to update status', e);
|
||||||
|
}
|
||||||
|
this.updating = false;
|
||||||
|
}
|
||||||
|
}">
|
||||||
|
<div class="px-3 py-2 flex flex-wrap gap-1.5">
|
||||||
|
{{-- Online --}}
|
||||||
|
<button
|
||||||
|
@click="setStatus('online')"
|
||||||
|
:disabled="updating"
|
||||||
|
class="badge badge-sm cursor-pointer transition-all"
|
||||||
|
:class="status === 'online'
|
||||||
|
? 'badge-success ring-2 ring-success/30'
|
||||||
|
: 'badge-ghost hover:badge-success'">
|
||||||
|
<span class="size-1.5 rounded-full mr-1"
|
||||||
|
:class="status === 'online' ? 'bg-success-content' : 'bg-success'"></span>
|
||||||
|
Online
|
||||||
|
</button>
|
||||||
|
{{-- Away --}}
|
||||||
|
<button
|
||||||
|
@click="setStatus('away')"
|
||||||
|
:disabled="updating"
|
||||||
|
class="badge badge-sm cursor-pointer transition-all"
|
||||||
|
:class="status === 'away'
|
||||||
|
? 'badge-warning ring-2 ring-warning/30'
|
||||||
|
: 'badge-ghost hover:badge-warning'">
|
||||||
|
<span class="size-1.5 rounded-full mr-1"
|
||||||
|
:class="status === 'away' ? 'bg-warning-content' : 'bg-warning'"></span>
|
||||||
|
Away
|
||||||
|
</button>
|
||||||
|
{{-- Busy --}}
|
||||||
|
<button
|
||||||
|
@click="setStatus('busy')"
|
||||||
|
:disabled="updating"
|
||||||
|
class="badge badge-sm cursor-pointer transition-all"
|
||||||
|
:class="status === 'busy'
|
||||||
|
? 'badge-error ring-2 ring-error/30'
|
||||||
|
: 'badge-ghost hover:badge-error'">
|
||||||
|
<span class="size-1.5 rounded-full mr-1"
|
||||||
|
:class="status === 'busy' ? 'bg-error-content' : 'bg-error'"></span>
|
||||||
|
Busy
|
||||||
|
</button>
|
||||||
|
{{-- Offline --}}
|
||||||
|
<button
|
||||||
|
@click="setStatus('offline')"
|
||||||
|
:disabled="updating"
|
||||||
|
class="badge badge-sm cursor-pointer transition-all"
|
||||||
|
:class="status === 'offline'
|
||||||
|
? 'badge-neutral ring-2 ring-neutral/30'
|
||||||
|
: 'badge-ghost hover:badge-neutral'">
|
||||||
|
<span class="size-1.5 rounded-full mr-1"
|
||||||
|
:class="status === 'offline' ? 'bg-neutral-content' : 'bg-base-content/30'"></span>
|
||||||
|
Offline
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
@endif
|
||||||
|
|
||||||
{{-- Admin Console Section (Owner/Admin Only) --}}
|
{{-- Admin Console Section (Owner/Admin Only) --}}
|
||||||
@if($canManageSettings && $business)
|
@if($canManageSettings && $business)
|
||||||
<li class="pt-2 mt-2 border-t border-base-200">
|
<li class="pt-2 mt-2 border-t border-base-200">
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
@extends('layouts.app-with-sidebar')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="nx-shell">
|
||||||
|
<div class="nx-page">
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<div class="flex items-center gap-4 mb-6">
|
||||||
|
<a href="{{ route('seller.business.crm.settings.index', $business) }}" class="btn btn-ghost btn-sm btn-circle">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold">Quick Replies</h1>
|
||||||
|
<p class="text-sm text-base-content/60">Pre-written responses for faster customer communication</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Create Quick Reply Form --}}
|
||||||
|
<div class="card bg-base-200 mb-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-lg">Create Quick Reply</h2>
|
||||||
|
<form method="POST" action="{{ route('seller.business.crm.settings.quick-replies.store', $business) }}" class="space-y-4">
|
||||||
|
@csrf
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label py-1"><span class="label-text">Label</span></label>
|
||||||
|
<input type="text" name="label" required class="input input-bordered input-sm" placeholder="e.g. Greeting, Thanks, Follow-up" />
|
||||||
|
<label class="label py-0"><span class="label-text-alt text-base-content/50">Short name to identify this reply</span></label>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label py-1"><span class="label-text">Category (optional)</span></label>
|
||||||
|
<input type="text" name="category" class="input input-bordered input-sm" placeholder="e.g. Support, Sales, General" list="category-suggestions" />
|
||||||
|
<datalist id="category-suggestions">
|
||||||
|
@foreach($categories as $cat)
|
||||||
|
<option value="{{ $cat }}">
|
||||||
|
@endforeach
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label py-1"><span class="label-text">Message</span></label>
|
||||||
|
<textarea name="message" required rows="3" class="textarea textarea-bordered text-sm" placeholder="Type your quick reply message here..."></textarea>
|
||||||
|
<label class="label py-0"><span class="label-text-alt text-base-content/50">Use @{{name}} for customer name, @{{product}} for product references</span></label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="label cursor-pointer gap-2">
|
||||||
|
<input type="checkbox" name="is_active" value="1" checked class="checkbox checkbox-sm checkbox-primary" />
|
||||||
|
<span class="label-text">Active</span>
|
||||||
|
</label>
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm">Add Quick Reply</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Existing Quick Replies --}}
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-lg">Your Quick Replies</h2>
|
||||||
|
|
||||||
|
@if($quickReplies->count() > 0)
|
||||||
|
<div class="space-y-3">
|
||||||
|
@foreach($quickReplies as $reply)
|
||||||
|
<div class="bg-base-100 rounded-lg p-4" x-data="{ editing: false }">
|
||||||
|
{{-- View Mode --}}
|
||||||
|
<div x-show="!editing" class="flex items-start gap-4">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<span class="font-semibold">{{ $reply->label }}</span>
|
||||||
|
@if($reply->category)
|
||||||
|
<span class="badge badge-ghost badge-sm">{{ $reply->category }}</span>
|
||||||
|
@endif
|
||||||
|
@if(!$reply->is_active)
|
||||||
|
<span class="badge badge-warning badge-sm">Inactive</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-base-content/70 whitespace-pre-wrap">{{ $reply->message }}</p>
|
||||||
|
<div class="mt-2 text-xs text-base-content/50">
|
||||||
|
Used {{ $reply->usage_count }} times
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button @click="editing = true" class="btn btn-ghost btn-sm btn-square">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<form method="POST" action="{{ route('seller.business.crm.settings.quick-replies.destroy', [$business, $reply]) }}"
|
||||||
|
onsubmit="return confirm('Delete this quick reply?')">
|
||||||
|
@csrf @method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-ghost btn-sm btn-square text-error">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Edit Mode --}}
|
||||||
|
<form x-show="editing" x-cloak method="POST" action="{{ route('seller.business.crm.settings.quick-replies.update', [$business, $reply]) }}" class="space-y-3">
|
||||||
|
@csrf @method('PATCH')
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label py-1"><span class="label-text">Label</span></label>
|
||||||
|
<input type="text" name="label" value="{{ $reply->label }}" required class="input input-bordered input-sm" />
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label py-1"><span class="label-text">Category</span></label>
|
||||||
|
<input type="text" name="category" value="{{ $reply->category }}" class="input input-bordered input-sm" list="category-suggestions" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label py-1"><span class="label-text">Message</span></label>
|
||||||
|
<textarea name="message" required rows="3" class="textarea textarea-bordered text-sm">{{ $reply->message }}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="label cursor-pointer gap-2">
|
||||||
|
<input type="checkbox" name="is_active" value="1" {{ $reply->is_active ? 'checked' : '' }} class="checkbox checkbox-sm checkbox-primary" />
|
||||||
|
<span class="label-text">Active</span>
|
||||||
|
</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button type="button" @click="editing = false" class="btn btn-ghost btn-sm">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="text-center py-8 text-base-content/50">
|
||||||
|
<svg class="w-12 h-12 mx-auto mb-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
|
||||||
|
</svg>
|
||||||
|
<p>No quick replies yet.</p>
|
||||||
|
<p class="text-sm">Create your first one above to speed up customer responses!</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -1027,6 +1027,14 @@ Route::prefix('s')->name('seller.')->middleware('seller')->group(function () {
|
|||||||
Route::patch('/{role}', [\App\Http\Controllers\Seller\Crm\CrmSettingsController::class, 'updateTeamRole'])->name('update');
|
Route::patch('/{role}', [\App\Http\Controllers\Seller\Crm\CrmSettingsController::class, 'updateTeamRole'])->name('update');
|
||||||
Route::delete('/{role}', [\App\Http\Controllers\Seller\Crm\CrmSettingsController::class, 'destroyTeamRole'])->name('destroy');
|
Route::delete('/{role}', [\App\Http\Controllers\Seller\Crm\CrmSettingsController::class, 'destroyTeamRole'])->name('destroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Quick Replies - chat quick reply templates
|
||||||
|
Route::prefix('quick-replies')->name('quick-replies.')->group(function () {
|
||||||
|
Route::get('/', [\App\Http\Controllers\Seller\Crm\CrmSettingsController::class, 'quickReplies'])->name('index');
|
||||||
|
Route::post('/', [\App\Http\Controllers\Seller\Crm\CrmSettingsController::class, 'storeQuickReply'])->name('store');
|
||||||
|
Route::patch('/{quickReply}', [\App\Http\Controllers\Seller\Crm\CrmSettingsController::class, 'updateQuickReply'])->name('update');
|
||||||
|
Route::delete('/{quickReply}', [\App\Http\Controllers\Seller\Crm\CrmSettingsController::class, 'destroyQuickReply'])->name('destroy');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -141,6 +141,9 @@ Route::middleware('auth')->group(function () {
|
|||||||
// View As (impersonation) routes
|
// View As (impersonation) routes
|
||||||
Route::post('/view-as/end', [\App\Http\Controllers\ViewAsController::class, 'end'])->name('view-as.end');
|
Route::post('/view-as/end', [\App\Http\Controllers\ViewAsController::class, 'end'])->name('view-as.end');
|
||||||
Route::get('/view-as/status', [\App\Http\Controllers\ViewAsController::class, 'status'])->name('view-as.status');
|
Route::get('/view-as/status', [\App\Http\Controllers\ViewAsController::class, 'status'])->name('view-as.status');
|
||||||
|
|
||||||
|
// Agent status for chat availability
|
||||||
|
Route::post('/api/agent-status', [\App\Http\Controllers\Api\AgentStatusController::class, 'update'])->name('api.agent-status.update');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Marketplace routes (browsing other businesses - marketplace style)
|
// Marketplace routes (browsing other businesses - marketplace style)
|
||||||
|
|||||||
Reference in New Issue
Block a user