Commit 7723c259 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add WhatsApp messaging via Meta Graph API (test mode)

- WhatsAppService: send text, template, image, document messages
- Bulk send to groups/status/custom selections
- Message log with status tracking (sent/delivered/read/failed)
- Three Livewire pages: Send Message, Send Template, Message Log
- Migration for whatsapp_messages table
- Added 'whatsapp' channel to NotificationChannel enum
- Wired into NotificationService for automated notifications
- Sidebar section "واتساب" with 3 items
- Config via WHATSAPP_PHONE_NUMBER_ID + WHATSAPP_ACCESS_TOKEN env vars
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 45593f71
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
case InApp = 'in_app'; case InApp = 'in_app';
case Email = 'email'; case Email = 'email';
case Sms = 'sms'; case Sms = 'sms';
case Whatsapp = 'whatsapp';
public function label(): string public function label(): string
{ {
...@@ -14,6 +15,7 @@ public function label(): string ...@@ -14,6 +15,7 @@ public function label(): string
self::InApp => 'إشعار داخلي', self::InApp => 'إشعار داخلي',
self::Email => 'بريد إلكتروني', self::Email => 'بريد إلكتروني',
self::Sms => 'رسالة نصية', self::Sms => 'رسالة نصية',
self::Whatsapp => 'واتساب',
}; };
} }
} }
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
use App\Domain\Notification\Models\NotificationPreference; use App\Domain\Notification\Models\NotificationPreference;
use App\Domain\Notification\Models\NotificationTemplate; use App\Domain\Notification\Models\NotificationTemplate;
use App\Domain\Shared\Exceptions\DomainException; use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\WhatsApp\Services\WhatsAppService;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
...@@ -69,6 +70,9 @@ public function send( ...@@ -69,6 +70,9 @@ public function send(
NotificationChannel::Sms => $this->sendSms( NotificationChannel::Sms => $this->sendSms(
$recipientPhone, $rendered, $recipientType, $recipientId, $academyId, $eventType $recipientPhone, $rendered, $recipientType, $recipientId, $academyId, $eventType
), ),
NotificationChannel::Whatsapp => $this->sendWhatsapp(
$recipientPhone, $rendered, $recipientType, $recipientId, $academyId, $eventType
),
}; };
$results[] = ['channel' => $channel->value, 'status' => 'sent']; $results[] = ['channel' => $channel->value, 'status' => 'sent'];
...@@ -360,6 +364,52 @@ private function sendSms( ...@@ -360,6 +364,52 @@ private function sendSms(
); );
} }
private function sendWhatsapp(
?string $phone,
array $rendered,
string $recipientType,
int $recipientId,
int $academyId,
string $eventType
): void {
if (!$phone) {
$this->logNotification(
$academyId, $recipientType, $recipientId, $eventType,
NotificationChannel::Whatsapp, null, $rendered['body'],
NotificationStatus::Failed, 'No phone number'
);
return;
}
$whatsapp = app(WhatsAppService::class);
if (!$whatsapp->isConfigured()) {
$this->logNotification(
$academyId, $recipientType, $recipientId, $eventType,
NotificationChannel::Whatsapp, null, $rendered['body'],
NotificationStatus::Failed, 'WhatsApp API not configured'
);
return;
}
$msg = $whatsapp->sendText(
to: $phone,
body: $rendered['body'],
academyId: $academyId,
senderId: auth()->id(),
);
$status = $msg->status === \App\Domain\WhatsApp\Enums\MessageStatus::Sent
? NotificationStatus::Sent
: NotificationStatus::Failed;
$this->logNotification(
$academyId, $recipientType, $recipientId, $eventType,
NotificationChannel::Whatsapp, null, $rendered['body'],
$status, $msg->error_message, ['wa_message_id' => $msg->wa_message_id]
);
}
/** /**
* Format Egyptian phone number to E.164 (+20...). * Format Egyptian phone number to E.164 (+20...).
*/ */
......
<?php
namespace App\Domain\WhatsApp\Enums;
enum MessageStatus: string
{
case Pending = 'pending';
case Sent = 'sent';
case Delivered = 'delivered';
case Read = 'read';
case Failed = 'failed';
public function label(): string
{
return match ($this) {
self::Pending => 'قيد الإرسال',
self::Sent => 'مُرسل',
self::Delivered => 'تم التسليم',
self::Read => 'مقروء',
self::Failed => 'فشل',
};
}
public function color(): string
{
return match ($this) {
self::Pending => 'gray',
self::Sent => 'blue',
self::Delivered => 'indigo',
self::Read => 'green',
self::Failed => 'red',
};
}
}
<?php
namespace App\Domain\WhatsApp\Enums;
enum MessageType: string
{
case Template = 'template';
case Text = 'text';
case Image = 'image';
case Document = 'document';
public function label(): string
{
return match ($this) {
self::Template => 'قالب',
self::Text => 'نص',
self::Image => 'صورة',
self::Document => 'مستند',
};
}
}
<?php
namespace App\Domain\WhatsApp\Models;
use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\WhatsApp\Enums\MessageStatus;
use App\Domain\WhatsApp\Enums\MessageType;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class WhatsAppMessage extends Model
{
use BelongsToAcademy;
protected $table = 'whatsapp_messages';
protected $fillable = [
'academy_id',
'wa_message_id',
'phone_number',
'type',
'content',
'status',
'error_message',
'payload',
'response',
'sent_by',
'sent_at',
'delivered_at',
'read_at',
];
protected function casts(): array
{
return [
'type' => MessageType::class,
'status' => MessageStatus::class,
'payload' => 'array',
'response' => 'array',
'sent_at' => 'datetime',
'delivered_at' => 'datetime',
'read_at' => 'datetime',
];
}
public function sender(): BelongsTo
{
return $this->belongsTo(\App\Models\User::class, 'sent_by');
}
}
<?php
namespace App\Domain\WhatsApp\Services;
use App\Domain\WhatsApp\Enums\MessageStatus;
use App\Domain\WhatsApp\Enums\MessageType;
use App\Domain\WhatsApp\Models\WhatsAppMessage;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class WhatsAppService
{
private string $apiVersion;
private string $phoneNumberId;
private string $accessToken;
private string $baseUrl;
public function __construct()
{
$this->apiVersion = config('services.whatsapp.api_version', 'v25.0');
$this->phoneNumberId = config('services.whatsapp.phone_number_id', '');
$this->accessToken = config('services.whatsapp.access_token', '');
$this->baseUrl = "https://graph.facebook.com/{$this->apiVersion}/{$this->phoneNumberId}";
}
public function sendTemplate(
string $to,
string $templateName,
string $languageCode = 'en_US',
array $components = [],
?int $academyId = null,
?int $senderId = null,
): WhatsAppMessage {
$to = $this->formatPhone($to);
$payload = [
'messaging_product' => 'whatsapp',
'to' => $to,
'type' => 'template',
'template' => [
'name' => $templateName,
'language' => ['code' => $languageCode],
],
];
if (!empty($components)) {
$payload['template']['components'] = $components;
}
$response = $this->sendRequest($payload);
return $this->logMessage(
academyId: $academyId,
to: $to,
type: MessageType::Template,
content: $templateName,
payload: $payload,
response: $response,
senderId: $senderId,
);
}
public function sendText(
string $to,
string $body,
?int $academyId = null,
?int $senderId = null,
bool $previewUrl = false,
): WhatsAppMessage {
$to = $this->formatPhone($to);
$payload = [
'messaging_product' => 'whatsapp',
'to' => $to,
'type' => 'text',
'text' => [
'preview_url' => $previewUrl,
'body' => $body,
],
];
$response = $this->sendRequest($payload);
return $this->logMessage(
academyId: $academyId,
to: $to,
type: MessageType::Text,
content: $body,
payload: $payload,
response: $response,
senderId: $senderId,
);
}
public function sendImage(
string $to,
string $imageUrl,
?string $caption = null,
?int $academyId = null,
?int $senderId = null,
): WhatsAppMessage {
$to = $this->formatPhone($to);
$payload = [
'messaging_product' => 'whatsapp',
'to' => $to,
'type' => 'image',
'image' => [
'link' => $imageUrl,
],
];
if ($caption) {
$payload['image']['caption'] = $caption;
}
$response = $this->sendRequest($payload);
return $this->logMessage(
academyId: $academyId,
to: $to,
type: MessageType::Image,
content: $caption ?? $imageUrl,
payload: $payload,
response: $response,
senderId: $senderId,
);
}
public function sendDocument(
string $to,
string $documentUrl,
?string $filename = null,
?string $caption = null,
?int $academyId = null,
?int $senderId = null,
): WhatsAppMessage {
$to = $this->formatPhone($to);
$payload = [
'messaging_product' => 'whatsapp',
'to' => $to,
'type' => 'document',
'document' => [
'link' => $documentUrl,
],
];
if ($filename) {
$payload['document']['filename'] = $filename;
}
if ($caption) {
$payload['document']['caption'] = $caption;
}
$response = $this->sendRequest($payload);
return $this->logMessage(
academyId: $academyId,
to: $to,
type: MessageType::Document,
content: $caption ?? $filename ?? $documentUrl,
payload: $payload,
response: $response,
senderId: $senderId,
);
}
public function sendBulkTemplate(
array $recipients,
string $templateName,
string $languageCode = 'en_US',
array $components = [],
?int $academyId = null,
?int $senderId = null,
): array {
$results = ['sent' => 0, 'failed' => 0, 'messages' => []];
foreach ($recipients as $phone) {
try {
$msg = $this->sendTemplate($phone, $templateName, $languageCode, $components, $academyId, $senderId);
if ($msg->status === MessageStatus::Failed) {
$results['failed']++;
} else {
$results['sent']++;
}
$results['messages'][] = $msg;
} catch (\Throwable $e) {
$results['failed']++;
Log::warning("WhatsApp bulk send failed for {$phone}: {$e->getMessage()}");
}
}
return $results;
}
public function sendBulkText(
array $recipients,
string $body,
?int $academyId = null,
?int $senderId = null,
): array {
$results = ['sent' => 0, 'failed' => 0, 'messages' => []];
foreach ($recipients as $phone) {
try {
$msg = $this->sendText($phone, $body, $academyId, $senderId);
if ($msg->status === MessageStatus::Failed) {
$results['failed']++;
} else {
$results['sent']++;
}
$results['messages'][] = $msg;
} catch (\Throwable $e) {
$results['failed']++;
Log::warning("WhatsApp bulk text failed for {$phone}: {$e->getMessage()}");
}
}
return $results;
}
public function isConfigured(): bool
{
return !empty($this->phoneNumberId) && !empty($this->accessToken);
}
private function sendRequest(array $payload): array
{
if (!$this->isConfigured()) {
return ['error' => ['message' => 'WhatsApp API not configured']];
}
$response = Http::withToken($this->accessToken)
->timeout(30)
->post("{$this->baseUrl}/messages", $payload);
$data = $response->json();
if (!$response->successful()) {
Log::error('WhatsApp API error', [
'status' => $response->status(),
'body' => $data,
'payload_to' => $payload['to'] ?? null,
]);
}
return $data ?? [];
}
private function logMessage(
?int $academyId,
string $to,
MessageType $type,
string $content,
array $payload,
array $response,
?int $senderId,
): WhatsAppMessage {
$waMessageId = $response['messages'][0]['id'] ?? null;
$status = $waMessageId ? MessageStatus::Sent : MessageStatus::Failed;
$errorMessage = null;
if (!$waMessageId) {
$errorMessage = $response['error']['message']
?? $response['error']['error_data']['details'] ?? 'Unknown error';
}
return WhatsAppMessage::create([
'academy_id' => $academyId ?? (int) app('current_academy')?->id,
'wa_message_id' => $waMessageId,
'phone_number' => $to,
'type' => $type->value,
'content' => mb_substr($content, 0, 2000),
'status' => $status->value,
'error_message' => $errorMessage,
'payload' => $payload,
'response' => $response,
'sent_by' => $senderId ?? auth()->id(),
'sent_at' => $waMessageId ? now() : null,
]);
}
private function formatPhone(string $phone): string
{
$phone = preg_replace('/[\s\-\(\)\+]/', '', $phone);
if (str_starts_with($phone, '0')) {
$phone = '20' . substr($phone, 1);
} elseif (strlen($phone) === 10) {
$phone = '20' . $phone;
}
return $phone;
}
}
<?php
namespace App\Livewire\WhatsApp;
use App\Domain\WhatsApp\Enums\MessageStatus;
use App\Domain\WhatsApp\Enums\MessageType;
use App\Domain\WhatsApp\Models\WhatsAppMessage;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
#[Layout('layouts.app')]
#[Title('سجل رسائل الواتساب')]
class MessageLog extends Component
{
use WithPagination;
#[Url]
public string $search = '';
#[Url]
public string $status = '';
#[Url]
public string $type = '';
public function mount(): void
{
$this->authorize('notifications.manage');
}
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedStatus(): void
{
$this->resetPage();
}
public function updatedType(): void
{
$this->resetPage();
}
public function render()
{
$query = WhatsAppMessage::query()
->with('sender:id,name')
->when($this->search, function ($q) {
$q->where(function ($q2) {
$q2->where('phone_number', 'ilike', "%{$this->search}%")
->orWhere('content', 'ilike', "%{$this->search}%");
});
})
->when($this->status, fn ($q) => $q->where('status', $this->status))
->when($this->type, fn ($q) => $q->where('type', $this->type))
->orderByDesc('created_at');
return view('livewire.whatsapp.message-log', [
'messages' => $query->paginate(25),
'statuses' => MessageStatus::cases(),
'types' => MessageType::cases(),
]);
}
}
<?php
namespace App\Livewire\WhatsApp;
use App\Domain\Participant\Enums\ParticipantStatus;
use App\Domain\Participant\Models\Participant;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\TrainingGroup;
use App\Domain\WhatsApp\Services\WhatsAppService;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('إرسال واتساب')]
class SendMessage extends Component
{
use UsesBranchScope;
public string $sendMode = 'single';
public string $phone = '';
public string $messageBody = '';
public string $selectedGroup = '';
public string $selectedStatus = '';
public array $selectedParticipants = [];
public bool $isSending = false;
public int $sentCount = 0;
public int $failedCount = 0;
public bool $sendComplete = false;
public function mount(): void
{
$this->authorize('notifications.manage');
}
public function rules(): array
{
return [
'messageBody' => 'required|string|max:4096',
'sendMode' => 'required|in:single,group,status,custom',
'phone' => 'required_if:sendMode,single|nullable|string|max:20',
'selectedGroup' => 'required_if:sendMode,group',
'selectedStatus' => 'required_if:sendMode,status',
'selectedParticipants' => 'required_if:sendMode,custom|array',
];
}
public function messages(): array
{
return [
'messageBody.required' => 'نص الرسالة مطلوب',
'messageBody.max' => 'نص الرسالة يجب أن لا يتجاوز 4096 حرف',
'phone.required_if' => 'رقم الهاتف مطلوب',
'selectedGroup.required_if' => 'يجب اختيار مجموعة',
'selectedStatus.required_if' => 'يجب اختيار حالة',
'selectedParticipants.required_if' => 'يجب اختيار مشتركين',
];
}
public function send(): void
{
$this->validate();
$whatsapp = app(WhatsAppService::class);
if (!$whatsapp->isConfigured()) {
session()->flash('error', 'خدمة الواتساب غير مفعلة. تأكد من إعداد WHATSAPP_PHONE_NUMBER_ID و WHATSAPP_ACCESS_TOKEN');
return;
}
$this->isSending = true;
$this->sentCount = 0;
$this->failedCount = 0;
$academyId = (int) app('current_academy')?->id;
if ($this->sendMode === 'single') {
$msg = $whatsapp->sendText(
to: $this->phone,
body: $this->messageBody,
academyId: $academyId,
senderId: auth()->id(),
);
if ($msg->status->value === 'failed') {
$this->failedCount = 1;
session()->flash('error', 'فشل الإرسال: ' . ($msg->error_message ?? 'خطأ غير معروف'));
} else {
$this->sentCount = 1;
}
} else {
$phones = $this->getRecipientPhones();
if (empty($phones)) {
session()->flash('error', 'لا يوجد أرقام واتساب للمستلمين المحددين');
$this->isSending = false;
return;
}
$results = $whatsapp->sendBulkText(
recipients: $phones,
body: $this->messageBody,
academyId: $academyId,
senderId: auth()->id(),
);
$this->sentCount = $results['sent'];
$this->failedCount = $results['failed'];
}
$this->isSending = false;
$this->sendComplete = true;
if ($this->failedCount === 0 && $this->sentCount > 0) {
session()->flash('success', "تم إرسال الرسالة بنجاح إلى {$this->sentCount} مستلم");
} elseif ($this->sentCount > 0) {
session()->flash('warning', "تم الإرسال: {$this->sentCount} ناجح، {$this->failedCount} فشل");
}
}
public function resetForm(): void
{
$this->reset([
'messageBody', 'phone', 'selectedGroup', 'selectedStatus',
'selectedParticipants', 'sendComplete', 'sentCount', 'failedCount',
]);
$this->sendMode = 'single';
}
public function getRecipientCount(): int
{
if ($this->sendMode === 'single') {
return $this->phone ? 1 : 0;
}
return count($this->getRecipientPhones());
}
public function render()
{
$branchId = $this->getActiveBranchId();
$groups = TrainingGroup::query()
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereIn('status', ['active', 'forming', 'full'])
->orderBy('name_ar')
->get(['id', 'name_ar', 'name', 'current_count']);
$allParticipants = Participant::query()
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->with('person:id,name_ar,name,phone')
->whereIn('status', [
ParticipantStatus::Active->value,
ParticipantStatus::Registered->value,
])
->get(['id', 'person_id']);
$whatsapp = app(WhatsAppService::class);
return view('livewire.whatsapp.send-message', [
'groups' => $groups,
'statuses' => ParticipantStatus::cases(),
'allParticipants' => $allParticipants,
'isConfigured' => $whatsapp->isConfigured(),
]);
}
private function getRecipientPhones(): array
{
$branchId = $this->getActiveBranchId();
$query = Participant::query()
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->with('person:id,phone');
$participants = match ($this->sendMode) {
'group' => $query->whereHas('enrollments', function ($q) {
$q->where('training_group_id', $this->selectedGroup)
->where('status', 'active');
})->get(),
'status' => $query->where('status', $this->selectedStatus)->get(),
'custom' => $query->whereIn('id', $this->selectedParticipants)->get(),
default => collect(),
};
return $participants
->map(fn ($p) => $p->person?->phone)
->filter()
->unique()
->values()
->toArray();
}
}
<?php
namespace App\Livewire\WhatsApp;
use App\Domain\Participant\Enums\ParticipantStatus;
use App\Domain\Participant\Models\Participant;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\TrainingGroup;
use App\Domain\WhatsApp\Services\WhatsAppService;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('إرسال قالب واتساب')]
class SendTemplate extends Component
{
use UsesBranchScope;
public string $sendMode = 'single';
public string $phone = '';
public string $templateName = '';
public string $languageCode = 'en_US';
public string $selectedGroup = '';
public string $selectedStatus = '';
public array $selectedParticipants = [];
public bool $isSending = false;
public int $sentCount = 0;
public int $failedCount = 0;
public bool $sendComplete = false;
public function mount(): void
{
$this->authorize('notifications.manage');
}
public function rules(): array
{
return [
'templateName' => 'required|string|max:255',
'languageCode' => 'required|string|max:10',
'sendMode' => 'required|in:single,group,status,custom',
'phone' => 'required_if:sendMode,single|nullable|string|max:20',
'selectedGroup' => 'required_if:sendMode,group',
'selectedStatus' => 'required_if:sendMode,status',
'selectedParticipants' => 'required_if:sendMode,custom|array',
];
}
public function messages(): array
{
return [
'templateName.required' => 'اسم القالب مطلوب',
'phone.required_if' => 'رقم الهاتف مطلوب',
'selectedGroup.required_if' => 'يجب اختيار مجموعة',
'selectedStatus.required_if' => 'يجب اختيار حالة',
'selectedParticipants.required_if' => 'يجب اختيار مشتركين',
];
}
public function send(): void
{
$this->validate();
$whatsapp = app(WhatsAppService::class);
if (!$whatsapp->isConfigured()) {
session()->flash('error', 'خدمة الواتساب غير مفعلة');
return;
}
$this->isSending = true;
$this->sentCount = 0;
$this->failedCount = 0;
$academyId = (int) app('current_academy')?->id;
if ($this->sendMode === 'single') {
$msg = $whatsapp->sendTemplate(
to: $this->phone,
templateName: $this->templateName,
languageCode: $this->languageCode,
academyId: $academyId,
senderId: auth()->id(),
);
if ($msg->status->value === 'failed') {
$this->failedCount = 1;
session()->flash('error', 'فشل الإرسال: ' . ($msg->error_message ?? 'خطأ غير معروف'));
} else {
$this->sentCount = 1;
}
} else {
$phones = $this->getRecipientPhones();
if (empty($phones)) {
session()->flash('error', 'لا يوجد أرقام واتساب للمستلمين المحددين');
$this->isSending = false;
return;
}
$results = $whatsapp->sendBulkTemplate(
recipients: $phones,
templateName: $this->templateName,
languageCode: $this->languageCode,
academyId: $academyId,
senderId: auth()->id(),
);
$this->sentCount = $results['sent'];
$this->failedCount = $results['failed'];
}
$this->isSending = false;
$this->sendComplete = true;
if ($this->failedCount === 0 && $this->sentCount > 0) {
session()->flash('success', "تم إرسال القالب بنجاح إلى {$this->sentCount} مستلم");
} elseif ($this->sentCount > 0) {
session()->flash('warning', "تم الإرسال: {$this->sentCount} ناجح، {$this->failedCount} فشل");
}
}
public function resetForm(): void
{
$this->reset([
'phone', 'templateName', 'selectedGroup', 'selectedStatus',
'selectedParticipants', 'sendComplete', 'sentCount', 'failedCount',
]);
$this->sendMode = 'single';
$this->languageCode = 'en_US';
}
public function render()
{
$branchId = $this->getActiveBranchId();
$groups = TrainingGroup::query()
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereIn('status', ['active', 'forming', 'full'])
->orderBy('name_ar')
->get(['id', 'name_ar', 'name', 'current_count']);
$allParticipants = Participant::query()
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->with('person:id,name_ar,name,phone')
->whereIn('status', [
ParticipantStatus::Active->value,
ParticipantStatus::Registered->value,
])
->get(['id', 'person_id']);
$whatsapp = app(WhatsAppService::class);
return view('livewire.whatsapp.send-template', [
'groups' => $groups,
'statuses' => ParticipantStatus::cases(),
'allParticipants' => $allParticipants,
'isConfigured' => $whatsapp->isConfigured(),
]);
}
private function getRecipientPhones(): array
{
$branchId = $this->getActiveBranchId();
$query = Participant::query()
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->with('person:id,phone');
$participants = match ($this->sendMode) {
'group' => $query->whereHas('enrollments', function ($q) {
$q->where('training_group_id', $this->selectedGroup)
->where('status', 'active');
})->get(),
'status' => $query->where('status', $this->selectedStatus)->get(),
'custom' => $query->whereIn('id', $this->selectedParticipants)->get(),
default => collect(),
};
return $participants
->map(fn ($p) => $p->person?->phone)
->filter()
->unique()
->values()
->toArray();
}
}
...@@ -35,4 +35,11 @@ ...@@ -35,4 +35,11 @@
], ],
], ],
'whatsapp' => [
'api_version' => env('WHATSAPP_API_VERSION', 'v25.0'),
'phone_number_id' => env('WHATSAPP_PHONE_NUMBER_ID'),
'access_token' => env('WHATSAPP_ACCESS_TOKEN'),
'webhook_verify_token' => env('WHATSAPP_WEBHOOK_VERIFY_TOKEN'),
],
]; ];
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('whatsapp_messages', function (Blueprint $table) {
$table->id();
$table->foreignId('academy_id')->constrained('organizations');
$table->string('wa_message_id')->nullable()->index();
$table->string('phone_number', 20);
$table->string('type', 30);
$table->text('content');
$table->string('status', 30)->default('pending');
$table->text('error_message')->nullable();
$table->jsonb('payload')->default('{}');
$table->jsonb('response')->default('{}');
$table->foreignId('sent_by')->nullable()->constrained('users');
$table->timestamp('sent_at')->nullable();
$table->timestamp('delivered_at')->nullable();
$table->timestamp('read_at')->nullable();
$table->timestamps();
$table->index(['academy_id', 'created_at']);
$table->index(['phone_number', 'created_at']);
});
DB::statement("ALTER TABLE whatsapp_messages ADD CONSTRAINT whatsapp_messages_type_check CHECK (type IN ('template', 'text', 'image', 'document'))");
DB::statement("ALTER TABLE whatsapp_messages ADD CONSTRAINT whatsapp_messages_status_check CHECK (status IN ('pending', 'sent', 'delivered', 'read', 'failed'))");
}
public function down(): void
{
Schema::dropIfExists('whatsapp_messages');
}
};
...@@ -69,6 +69,12 @@ ...@@ -69,6 +69,12 @@
['label' => 'تعيين المساحات', 'route' => 'facilities.space-assignment', 'icon' => 'grid', 'permission' => 'facilities.manage_layouts'], ['label' => 'تعيين المساحات', 'route' => 'facilities.space-assignment', 'icon' => 'grid', 'permission' => 'facilities.manage_layouts'],
]], ]],
['section' => 'واتساب', 'items' => [
['label' => 'إرسال رسالة', 'route' => 'whatsapp.send', 'icon' => 'chat', 'permission' => 'notifications.manage'],
['label' => 'إرسال قالب', 'route' => 'whatsapp.templates', 'icon' => 'document-text', 'permission' => 'notifications.manage'],
['label' => 'سجل الرسائل', 'route' => 'whatsapp.log', 'icon' => 'eye', 'permission' => 'notifications.manage'],
]],
['section' => 'الإشعارات والرسائل', 'items' => [ ['section' => 'الإشعارات والرسائل', 'items' => [
['label' => 'مركز الإشعارات', 'route' => 'notifications.center', 'icon' => 'bolt', 'permission' => 'dashboard.view'], ['label' => 'مركز الإشعارات', 'route' => 'notifications.center', 'icon' => 'bolt', 'permission' => 'dashboard.view'],
['label' => 'رسائل جماعية', 'route' => 'messaging.bulk', 'icon' => 'chat', 'permission' => 'notifications.manage'], ['label' => 'رسائل جماعية', 'route' => 'messaging.bulk', 'icon' => 'chat', 'permission' => 'notifications.manage'],
......
<div>
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center">
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>
</div>
<h1 class="text-xl sm:text-2xl font-bold text-gray-800">{{ __('سجل رسائل الواتساب') }}</h1>
</div>
<a href="{{ route('whatsapp.send') }}" wire:navigate
class="inline-flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 text-sm font-medium">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
{{ __('رسالة جديدة') }}
</a>
</div>
{{-- Filters --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-4">
<div class="grid grid-cols-1 sm:grid-cols-4 gap-4">
<div class="sm:col-span-2">
<input type="text" wire:model.live.debounce.300ms="search"
placeholder="{{ __('بحث برقم الهاتف أو المحتوى...') }}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500">
</div>
<select wire:model.live="status" class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500">
<option value="">{{ __('كل الحالات') }}</option>
@foreach($statuses as $statusOption)
<option value="{{ $statusOption->value }}">{{ $statusOption->label() }}</option>
@endforeach
</select>
<select wire:model.live="type" class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500">
<option value="">{{ __('كل الأنواع') }}</option>
@foreach($types as $typeOption)
<option value="{{ $typeOption->value }}">{{ $typeOption->label() }}</option>
@endforeach
</select>
</div>
</div>
{{-- Table --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div wire:loading.class="opacity-50 pointer-events-none">
<table class="w-full text-sm">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th class="px-4 py-3 text-start font-medium text-gray-600">{{ __('الرقم') }}</th>
<th class="px-4 py-3 text-start font-medium text-gray-600">{{ __('المحتوى') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('النوع') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('الحالة') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('المرسل') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('التاريخ') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse($messages as $msg)
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 font-mono text-xs text-gray-700" dir="ltr">
{{ $msg->phone_number }}
</td>
<td class="px-4 py-3 text-gray-700 max-w-xs truncate">
{{ Str::limit($msg->content, 60) }}
</td>
<td class="px-4 py-3 text-center">
<span class="px-2 py-0.5 text-xs rounded-full bg-gray-100 text-gray-600">
{{ $msg->type->label() }}
</span>
</td>
<td class="px-4 py-3 text-center">
@php
$statusColor = match($msg->status) {
\App\Domain\WhatsApp\Enums\MessageStatus::Sent => 'bg-blue-100 text-blue-700',
\App\Domain\WhatsApp\Enums\MessageStatus::Delivered => 'bg-indigo-100 text-indigo-700',
\App\Domain\WhatsApp\Enums\MessageStatus::Read => 'bg-green-100 text-green-700',
\App\Domain\WhatsApp\Enums\MessageStatus::Failed => 'bg-red-100 text-red-700',
default => 'bg-gray-100 text-gray-700',
};
@endphp
<span class="px-2 py-0.5 text-xs rounded-full {{ $statusColor }}">
{{ $msg->status->label() }}
</span>
@if($msg->error_message)
<p class="text-xs text-red-500 mt-1 max-w-[150px] truncate" title="{{ $msg->error_message }}">
{{ $msg->error_message }}
</p>
@endif
</td>
<td class="px-4 py-3 text-center text-gray-500 text-xs">
{{ $msg->sender?->name ?? '-' }}
</td>
<td class="px-4 py-3 text-center text-gray-500 text-xs" dir="ltr">
{{ $msg->created_at->format('Y-m-d H:i') }}
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-4 py-12 text-center">
<div class="flex flex-col items-center">
<svg class="w-12 h-12 text-gray-300 mb-3" fill="currentColor" viewBox="0 0 24 24"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347z"/></svg>
<p class="text-gray-500 text-sm">{{ __('لا توجد رسائل بعد') }}</p>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($messages->hasPages())
<div class="px-4 py-3 border-t border-gray-200">
{{ $messages->links() }}
</div>
@endif
</div>
</div>
<div>
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center">
<svg class="w-5 h-5 text-green-600" fill="currentColor" viewBox="0 0 24 24"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347z"/></svg>
</div>
<h1 class="text-xl sm:text-2xl font-bold text-gray-800">{{ __('إرسال رسالة واتساب') }}</h1>
</div>
</div>
@if(!$isConfigured)
<div class="mb-6 p-4 bg-amber-50 border border-amber-200 rounded-lg">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-amber-500 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>
<div>
<p class="font-medium text-amber-800">{{ __('خدمة الواتساب غير مفعلة') }}</p>
<p class="text-sm text-amber-600 mt-1">{{ __('أضف WHATSAPP_PHONE_NUMBER_ID و WHATSAPP_ACCESS_TOKEN في متغيرات البيئة') }}</p>
</div>
</div>
</div>
@endif
@if(session('success'))
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm">{{ session('success') }}</div>
@endif
@if(session('error'))
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">{{ session('error') }}</div>
@endif
@if(session('warning'))
<div class="mb-4 p-4 bg-amber-50 border border-amber-200 rounded-lg text-amber-700 text-sm">{{ session('warning') }}</div>
@endif
<form wire:submit="send" class="space-y-6">
{{-- Recipient Selection --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-700 mb-4">{{ __('المستلمون') }}</h2>
<div class="flex flex-wrap gap-2 mb-4">
@foreach([
'single' => 'رقم محدد',
'group' => 'مجموعة تدريب',
'status' => 'حسب الحالة',
'custom' => 'اختيار يدوي',
] as $mode => $label)
<button type="button"
wire:click="$set('sendMode', '{{ $mode }}')"
class="px-4 py-2 rounded-lg text-sm font-medium transition {{ $sendMode === $mode ? 'bg-green-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200' }}">
{{ __($label) }}
</button>
@endforeach
</div>
@if($sendMode === 'single')
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('رقم الواتساب') }}</label>
<input type="text" wire:model="phone" dir="ltr"
class="w-full max-w-sm px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500"
placeholder="01060929653">
@error('phone') <span class="text-red-500 text-xs mt-1">{{ $message }}</span> @enderror
</div>
@elseif($sendMode === 'group')
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('المجموعة') }}</label>
<select wire:model="selectedGroup" class="w-full max-w-md px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500">
<option value="">{{ __('اختر المجموعة') }}</option>
@foreach($groups as $group)
<option value="{{ $group->id }}">{{ $group->name_ar }} ({{ $group->current_count }} مشترك)</option>
@endforeach
</select>
@error('selectedGroup') <span class="text-red-500 text-xs mt-1">{{ $message }}</span> @enderror
</div>
@elseif($sendMode === 'status')
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('الحالة') }}</label>
<select wire:model="selectedStatus" class="w-full max-w-md px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500">
<option value="">{{ __('اختر الحالة') }}</option>
@foreach($statuses as $statusOption)
<option value="{{ $statusOption->value }}">{{ $statusOption->label() }}</option>
@endforeach
</select>
@error('selectedStatus') <span class="text-red-500 text-xs mt-1">{{ $message }}</span> @enderror
</div>
@elseif($sendMode === 'custom')
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('اختر المشتركين') }}</label>
<div class="max-h-48 overflow-y-auto border border-gray-200 rounded-lg p-2 space-y-1">
@foreach($allParticipants as $participant)
@if($participant->person?->phone)
<label class="flex items-center gap-2 px-3 py-1.5 rounded hover:bg-gray-50 cursor-pointer">
<input type="checkbox" wire:model="selectedParticipants" value="{{ $participant->id }}"
class="rounded text-green-600 focus:ring-green-500">
<span class="text-sm text-gray-700">{{ $participant->person->name_ar ?? $participant->person->name }}</span>
<span class="text-xs text-gray-400 ms-auto" dir="ltr">{{ $participant->person->phone }}</span>
</label>
@endif
@endforeach
</div>
@error('selectedParticipants') <span class="text-red-500 text-xs mt-1">{{ $message }}</span> @enderror
</div>
@endif
</div>
{{-- Message Body --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-700 mb-4">{{ __('نص الرسالة') }}</h2>
<textarea wire:model="messageBody" rows="6"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 resize-none"
placeholder="{{ __('اكتب رسالتك هنا...') }}"></textarea>
<div class="flex items-center justify-between mt-2">
<span class="text-xs text-gray-400">{{ __('الحد الأقصى 4096 حرف') }}</span>
<span class="text-xs {{ strlen($messageBody) > 4096 ? 'text-red-500' : 'text-gray-400' }}">{{ strlen($messageBody) }}/4096</span>
</div>
@error('messageBody') <span class="text-red-500 text-xs mt-1">{{ $message }}</span> @enderror
</div>
{{-- Submit --}}
<div class="flex items-center justify-between">
@if($sendComplete)
<button type="button" wire:click="resetForm"
class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 text-sm">
{{ __('رسالة جديدة') }}
</button>
@else
<div></div>
@endif
<button type="submit"
wire:loading.attr="disabled"
wire:target="send"
{{ !$isConfigured ? 'disabled' : '' }}
class="px-6 py-2.5 bg-green-600 text-white rounded-lg hover:bg-green-700 text-sm font-medium disabled:opacity-50 flex items-center gap-2">
<span wire:loading.remove wire:target="send">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
</span>
<span wire:loading wire:target="send">
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
</span>
<span wire:loading.remove wire:target="send">{{ __('إرسال') }}</span>
<span wire:loading wire:target="send">{{ __('جارٍ الإرسال...') }}</span>
</button>
</div>
</form>
</div>
<div>
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center">
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
</div>
<h1 class="text-xl sm:text-2xl font-bold text-gray-800">{{ __('إرسال قالب واتساب') }}</h1>
</div>
</div>
@if(!$isConfigured)
<div class="mb-6 p-4 bg-amber-50 border border-amber-200 rounded-lg">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-amber-500 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>
<div>
<p class="font-medium text-amber-800">{{ __('خدمة الواتساب غير مفعلة') }}</p>
<p class="text-sm text-amber-600 mt-1">{{ __('أضف WHATSAPP_PHONE_NUMBER_ID و WHATSAPP_ACCESS_TOKEN في متغيرات البيئة') }}</p>
</div>
</div>
</div>
@endif
@if(session('success'))
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm">{{ session('success') }}</div>
@endif
@if(session('error'))
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">{{ session('error') }}</div>
@endif
@if(session('warning'))
<div class="mb-4 p-4 bg-amber-50 border border-amber-200 rounded-lg text-amber-700 text-sm">{{ session('warning') }}</div>
@endif
<form wire:submit="send" class="space-y-6">
{{-- Template Config --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-700 mb-4">{{ __('إعدادات القالب') }}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('اسم القالب') }} <span class="text-red-500">*</span></label>
<input type="text" wire:model="templateName" dir="ltr"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500"
placeholder="hello_world">
<p class="text-xs text-gray-400 mt-1">{{ __('اسم القالب المعتمد من Meta') }}</p>
@error('templateName') <span class="text-red-500 text-xs mt-1">{{ $message }}</span> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('لغة القالب') }}</label>
<select wire:model="languageCode" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500">
<option value="en_US">English (US)</option>
<option value="ar">العربية</option>
<option value="en">English</option>
</select>
</div>
</div>
</div>
{{-- Recipients --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-700 mb-4">{{ __('المستلمون') }}</h2>
<div class="flex flex-wrap gap-2 mb-4">
@foreach([
'single' => 'رقم محدد',
'group' => 'مجموعة تدريب',
'status' => 'حسب الحالة',
'custom' => 'اختيار يدوي',
] as $mode => $label)
<button type="button"
wire:click="$set('sendMode', '{{ $mode }}')"
class="px-4 py-2 rounded-lg text-sm font-medium transition {{ $sendMode === $mode ? 'bg-green-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200' }}">
{{ __($label) }}
</button>
@endforeach
</div>
@if($sendMode === 'single')
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('رقم الواتساب') }}</label>
<input type="text" wire:model="phone" dir="ltr"
class="w-full max-w-sm px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
placeholder="01060929653">
@error('phone') <span class="text-red-500 text-xs mt-1">{{ $message }}</span> @enderror
</div>
@elseif($sendMode === 'group')
<div>
<select wire:model="selectedGroup" class="w-full max-w-md px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500">
<option value="">{{ __('اختر المجموعة') }}</option>
@foreach($groups as $group)
<option value="{{ $group->id }}">{{ $group->name_ar }} ({{ $group->current_count }})</option>
@endforeach
</select>
@error('selectedGroup') <span class="text-red-500 text-xs mt-1">{{ $message }}</span> @enderror
</div>
@elseif($sendMode === 'status')
<div>
<select wire:model="selectedStatus" class="w-full max-w-md px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500">
<option value="">{{ __('اختر الحالة') }}</option>
@foreach($statuses as $statusOption)
<option value="{{ $statusOption->value }}">{{ $statusOption->label() }}</option>
@endforeach
</select>
@error('selectedStatus') <span class="text-red-500 text-xs mt-1">{{ $message }}</span> @enderror
</div>
@elseif($sendMode === 'custom')
<div class="max-h-48 overflow-y-auto border border-gray-200 rounded-lg p-2 space-y-1">
@foreach($allParticipants as $participant)
@if($participant->person?->phone)
<label class="flex items-center gap-2 px-3 py-1.5 rounded hover:bg-gray-50 cursor-pointer">
<input type="checkbox" wire:model="selectedParticipants" value="{{ $participant->id }}"
class="rounded text-green-600 focus:ring-green-500">
<span class="text-sm text-gray-700">{{ $participant->person->name_ar ?? $participant->person->name }}</span>
<span class="text-xs text-gray-400 ms-auto" dir="ltr">{{ $participant->person->phone }}</span>
</label>
@endif
@endforeach
</div>
@error('selectedParticipants') <span class="text-red-500 text-xs mt-1">{{ $message }}</span> @enderror
@endif
</div>
{{-- Submit --}}
<div class="flex items-center justify-between">
@if($sendComplete)
<button type="button" wire:click="resetForm"
class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 text-sm">
{{ __('إرسال جديد') }}
</button>
@else
<div></div>
@endif
<button type="submit"
wire:loading.attr="disabled"
wire:target="send"
{{ !$isConfigured ? 'disabled' : '' }}
class="px-6 py-2.5 bg-green-600 text-white rounded-lg hover:bg-green-700 text-sm font-medium disabled:opacity-50 flex items-center gap-2">
<span wire:loading.remove wire:target="send">{{ __('إرسال القالب') }}</span>
<span wire:loading wire:target="send">{{ __('جارٍ الإرسال...') }}</span>
</button>
</div>
</form>
</div>
...@@ -388,6 +388,14 @@ ...@@ -388,6 +388,14 @@
Route::get('/messaging', \App\Livewire\Messaging\BulkMessage::class)->name('messaging.bulk') Route::get('/messaging', \App\Livewire\Messaging\BulkMessage::class)->name('messaging.bulk')
->middleware('permission:notifications.manage'); ->middleware('permission:notifications.manage');
// WhatsApp
Route::get('/whatsapp/send', \App\Livewire\WhatsApp\SendMessage::class)->name('whatsapp.send')
->middleware('permission:notifications.manage');
Route::get('/whatsapp/templates', \App\Livewire\WhatsApp\SendTemplate::class)->name('whatsapp.templates')
->middleware('permission:notifications.manage');
Route::get('/whatsapp/log', \App\Livewire\WhatsApp\MessageLog::class)->name('whatsapp.log')
->middleware('permission:notifications.manage');
// Notifications // Notifications
Route::get('/notifications', NotificationCenter::class)->name('notifications.center'); Route::get('/notifications', NotificationCenter::class)->name('notifications.center');
Route::get('/notifications/templates', NotificationTemplateList::class)->name('notifications.templates') Route::get('/notifications/templates', NotificationTemplateList::class)->name('notifications.templates')
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment