333 lines
12 KiB
PHP
333 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\DB\Te\TableEngine;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\AppNotification;
|
|
use App\Models\AppNotificationRead;
|
|
use App\Models\AppNotificationRecipient;
|
|
use App\Models\Customer;
|
|
use App\Services\AppNotificationAudienceService;
|
|
use App\Services\PriceStreamService;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class AppNotificationController extends Controller
|
|
{
|
|
public function table(Request $request)
|
|
{
|
|
$query = AppNotification::query()->withCount('reads');
|
|
|
|
$source = trim((string) $request->input('source', 'all'));
|
|
$hasIsSystem = \Illuminate\Support\Facades\Schema::hasColumn('app_notifications', 'is_system');
|
|
|
|
if ($source === 'system') {
|
|
if ($hasIsSystem) {
|
|
$query->system();
|
|
} else {
|
|
$query->where(function ($q) {
|
|
$q->whereNull('created_by')
|
|
->orWhere('audience_label', 'like', 'سیستمی%');
|
|
});
|
|
}
|
|
} elseif ($source === 'admin') {
|
|
if ($hasIsSystem) {
|
|
$query->adminCreated();
|
|
} else {
|
|
$query->whereNotNull('created_by')
|
|
->where(function ($q) {
|
|
$q->whereNull('audience_label')
|
|
->orWhere('audience_label', 'not like', 'سیستمی%');
|
|
});
|
|
}
|
|
}
|
|
|
|
TableEngine::create($query, $request->all())
|
|
->initSearch(['title', 'body', 'severity', 'audience_label', 'audience_type'])
|
|
->select(['*'])
|
|
->export($query);
|
|
|
|
return $query;
|
|
}
|
|
|
|
public function audienceOptions()
|
|
{
|
|
return response()->json([
|
|
'segments' => app(AppNotificationAudienceService::class)->segmentOptions(),
|
|
]);
|
|
}
|
|
|
|
public function searchCustomers(Request $request)
|
|
{
|
|
$q = trim((string) $request->get('q', ''));
|
|
|
|
$rows = Customer::query()
|
|
->where('type', 2)
|
|
->where('is_archive', false)
|
|
->when($q !== '', function ($query) use ($q) {
|
|
$query->where(function ($inner) use ($q) {
|
|
$inner->where('name', 'like', '%' . $q . '%')
|
|
->orWhere('mobile', 'like', '%' . $q . '%')
|
|
->orWhere('id', $q);
|
|
});
|
|
})
|
|
->orderByDesc('id')
|
|
->limit(30)
|
|
->get(['id', 'name', 'mobile']);
|
|
|
|
return response()->json(['customers' => $rows]);
|
|
}
|
|
|
|
public function previewAudience(Request $request)
|
|
{
|
|
$audience = app(AppNotificationAudienceService::class);
|
|
$type = (string) $request->input('audience_type', AppNotificationAudienceService::TYPE_ALL);
|
|
$segmentKeys = array_values(array_filter((array) $request->input('segment_keys', [])));
|
|
$customerIds = array_values(array_filter(array_map('intval', (array) $request->input('customer_ids', []))));
|
|
|
|
$customers = match ($type) {
|
|
AppNotificationAudienceService::TYPE_SEGMENT => $audience->resolveSegmentCustomers($segmentKeys),
|
|
AppNotificationAudienceService::TYPE_CUSTOMERS => $audience->resolveCustomersByIds($customerIds),
|
|
default => collect(),
|
|
};
|
|
|
|
return response()->json([
|
|
'count' => $type === AppNotificationAudienceService::TYPE_ALL ? null : $customers->count(),
|
|
'sample' => $customers->take(20)->values(),
|
|
'label' => $audience->buildAudienceLabel($type, $segmentKeys, $customers->count()),
|
|
]);
|
|
}
|
|
|
|
public function store(Request $request)
|
|
{
|
|
$data = $this->validatedPayload($request);
|
|
$audience = app(AppNotificationAudienceService::class);
|
|
|
|
$audienceType = (string) ($data['audience_type'] ?? AppNotificationAudienceService::TYPE_ALL);
|
|
$segmentKeys = array_values(array_filter((array) ($data['segment_keys'] ?? [])));
|
|
$customerIds = array_values(array_filter(array_map('intval', (array) ($data['customer_ids'] ?? []))));
|
|
|
|
$recipients = match ($audienceType) {
|
|
AppNotificationAudienceService::TYPE_SEGMENT => $audience->resolveSegmentCustomers($segmentKeys),
|
|
AppNotificationAudienceService::TYPE_CUSTOMERS => $audience->resolveCustomersByIds($customerIds),
|
|
default => collect(),
|
|
};
|
|
|
|
if ($audienceType === AppNotificationAudienceService::TYPE_SEGMENT && $recipients->isEmpty()) {
|
|
return response()->json(['message' => 'برای گروه انتخابشده مشتریای یافت نشد.'], 422);
|
|
}
|
|
if ($audienceType === AppNotificationAudienceService::TYPE_CUSTOMERS && $recipients->isEmpty()) {
|
|
return response()->json(['message' => 'حداقل یک مشتری انتخاب کنید.'], 422);
|
|
}
|
|
|
|
$recipientsCount = $audienceType === AppNotificationAudienceService::TYPE_ALL
|
|
? 0
|
|
: $recipients->count();
|
|
|
|
$row = DB::transaction(function () use ($data, $audience, $audienceType, $segmentKeys, $recipients, $recipientsCount) {
|
|
$user = auth()->user();
|
|
|
|
$payload = [
|
|
'title' => $data['title'],
|
|
'body' => $data['body'],
|
|
'severity' => $data['severity'],
|
|
'is_active' => $data['is_active'] ?? true,
|
|
'created_by' => $user?->id,
|
|
'audience_type' => $audienceType,
|
|
'segment_keys' => $audienceType === AppNotificationAudienceService::TYPE_SEGMENT ? $segmentKeys : null,
|
|
'audience_label' => $audience->buildAudienceLabel($audienceType, $segmentKeys, $recipientsCount),
|
|
'recipients_count' => $recipientsCount,
|
|
];
|
|
if (\Illuminate\Support\Facades\Schema::hasColumn('app_notifications', 'is_system')) {
|
|
$payload['is_system'] = false;
|
|
}
|
|
|
|
$row = AppNotification::create($payload);
|
|
|
|
if ($audienceType !== AppNotificationAudienceService::TYPE_ALL) {
|
|
$now = now();
|
|
$chunks = $recipients->pluck('id')->map(fn ($id) => [
|
|
'notification_id' => $row->id,
|
|
'customer_id' => (int) $id,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
])->chunk(500);
|
|
|
|
foreach ($chunks as $chunk) {
|
|
AppNotificationRecipient::query()->insert($chunk->values()->all());
|
|
}
|
|
}
|
|
|
|
return $row;
|
|
});
|
|
|
|
$this->broadcastNotification($row);
|
|
|
|
return response()->json($row->fresh(), 201);
|
|
}
|
|
|
|
public function update(Request $request, int $id)
|
|
{
|
|
$row = AppNotification::findOrFail($id);
|
|
$isSystem = \Illuminate\Support\Facades\Schema::hasColumn('app_notifications', 'is_system')
|
|
? (bool) $row->is_system
|
|
: ($row->created_by === null && str_starts_with((string) $row->audience_label, 'سیستمی'));
|
|
if ($isSystem) {
|
|
return response()->json(['message' => 'اعلان سیستمی قابل ویرایش نیست.'], 422);
|
|
}
|
|
// مخاطب بعد از ارسال ثابت میماند — فقط محتوا/وضعیت
|
|
$data = $request->validate([
|
|
'title' => 'sometimes|required|string|max:191',
|
|
'body' => 'sometimes|required|string|max:5000',
|
|
'severity' => 'sometimes|required|in:info,success,warning,danger',
|
|
'is_active' => 'sometimes|boolean',
|
|
]);
|
|
$row->update($data);
|
|
|
|
return response()->json(true);
|
|
}
|
|
|
|
public function toggleActive(int $id)
|
|
{
|
|
$row = AppNotification::findOrFail($id);
|
|
$row->is_active = ! $row->is_active;
|
|
$row->save();
|
|
|
|
return response()->json([
|
|
'is_active' => (bool) $row->is_active,
|
|
]);
|
|
}
|
|
|
|
public function destroy(int $id)
|
|
{
|
|
$row = AppNotification::findOrFail($id);
|
|
AppNotificationRecipient::query()->where('notification_id', $id)->delete();
|
|
$row->delete();
|
|
|
|
return response()->json(true);
|
|
}
|
|
|
|
/** بازدیدکنندگان (خواندهاند) */
|
|
public function readers(int $id)
|
|
{
|
|
AppNotification::findOrFail($id);
|
|
|
|
$rows = AppNotificationRead::query()
|
|
->where('notification_id', $id)
|
|
->with(['customer:id,name,fname,lname,mobile'])
|
|
->orderByDesc('read_at')
|
|
->orderByDesc('id')
|
|
->get();
|
|
|
|
$readers = $rows->map(function (AppNotificationRead $read) {
|
|
$customer = $read->customer;
|
|
$name = trim((string) ($customer?->name ?: trim(($customer?->fname ?? '') . ' ' . ($customer?->lname ?? ''))));
|
|
|
|
return [
|
|
'customer_id' => $read->customer_id,
|
|
'name' => $name !== '' ? $name : '—',
|
|
'mobile' => $customer?->mobile,
|
|
'read_at' => $read->read_at,
|
|
];
|
|
})->values();
|
|
|
|
return response()->json([
|
|
'reads_count' => $readers->count(),
|
|
'readers' => $readers,
|
|
]);
|
|
}
|
|
|
|
/** گیرندگان هدف (اسنپشات ارسال) */
|
|
public function recipients(int $id)
|
|
{
|
|
$row = AppNotification::findOrFail($id);
|
|
|
|
if ($row->audience_type === AppNotificationAudienceService::TYPE_ALL) {
|
|
return response()->json([
|
|
'audience_type' => $row->audience_type,
|
|
'audience_label' => $row->audience_label ?: 'همه کاربران',
|
|
'recipients_count' => null,
|
|
'recipients' => [],
|
|
'note' => 'این اعلان برای همه کاربران فعال ارسال شده است.',
|
|
]);
|
|
}
|
|
|
|
$rows = AppNotificationRecipient::query()
|
|
->where('notification_id', $id)
|
|
->with(['customer:id,name,fname,lname,mobile'])
|
|
->orderByDesc('id')
|
|
->limit(500)
|
|
->get();
|
|
|
|
$recipients = $rows->map(function (AppNotificationRecipient $item) {
|
|
$customer = $item->customer;
|
|
$name = trim((string) ($customer?->name ?: trim(($customer?->fname ?? '') . ' ' . ($customer?->lname ?? ''))));
|
|
|
|
return [
|
|
'customer_id' => $item->customer_id,
|
|
'name' => $name !== '' ? $name : '—',
|
|
'mobile' => $customer?->mobile,
|
|
];
|
|
})->values();
|
|
|
|
return response()->json([
|
|
'audience_type' => $row->audience_type,
|
|
'audience_label' => $row->audience_label,
|
|
'recipients_count' => (int) $row->recipients_count,
|
|
'recipients' => $recipients,
|
|
]);
|
|
}
|
|
|
|
private function validatedPayload(Request $request): array
|
|
{
|
|
return $request->validate([
|
|
'title' => 'required|string|max:191',
|
|
'body' => 'required|string|max:5000',
|
|
'severity' => 'required|in:info,success,warning,danger',
|
|
'is_active' => 'sometimes|boolean',
|
|
'audience_type' => 'required|in:all,segment,customers',
|
|
'segment_keys' => 'nullable|array',
|
|
'segment_keys.*' => 'string',
|
|
'customer_ids' => 'nullable|array',
|
|
'customer_ids.*' => 'integer',
|
|
]);
|
|
}
|
|
|
|
private function broadcastNotification(AppNotification $row): void
|
|
{
|
|
if (! $row->is_active) {
|
|
return;
|
|
}
|
|
|
|
$payload = [
|
|
'id' => $row->id,
|
|
'title' => $row->title,
|
|
'body' => $row->body,
|
|
'severity' => $row->severity,
|
|
'created_at' => optional($row->created_at)?->toIso8601String(),
|
|
];
|
|
|
|
$stream = app(PriceStreamService::class);
|
|
$unread = app(\App\Services\AppNotificationUnreadService::class);
|
|
|
|
if ($row->audience_type === AppNotificationAudienceService::TYPE_ALL) {
|
|
$stream->publish('app_notification', $payload);
|
|
|
|
return;
|
|
}
|
|
|
|
$ids = AppNotificationRecipient::query()
|
|
->where('notification_id', $row->id)
|
|
->pluck('customer_id');
|
|
|
|
foreach ($ids as $customerId) {
|
|
$cid = (int) $customerId;
|
|
$stream->publishForCustomer($cid, 'app_notification', array_merge($payload, [
|
|
'unread_count' => $unread->countForCustomer($cid),
|
|
]));
|
|
$unread->publishForCustomer($cid);
|
|
}
|
|
}
|
|
}
|