get('per_page', 20), 200); $paginator = MailCampaign::query() ->with(['template', 'creator']) ->orderByDesc('created_at') ->paginate($limit); return response()->json([ 'error' => false, 'data' => [ 'items' => $paginator->getCollection()->map($this->mapCampaign(...))->all(), 'total' => $paginator->total(), 'per_page' => $paginator->perPage(), 'current_page' => $paginator->currentPage(), 'last_page' => $paginator->lastPage(), ], ]); } public function show(string $uuid): JsonResponse { $campaign = MailCampaign::with(['template', 'creator'])->where('uuid', $uuid)->first(); if (!$campaign) { return response()->json(['error' => true, 'message' => 'Không tìm thấy chiến dịch'], 404); } $logs = Mail::with('customer') ->where('campaign_id', $campaign->id) ->orderByDesc('created_at') ->get(['id', 'to', 'customer_id', 'subject', 'status', 'created_at']) ->map(fn (Mail $log) => [ 'id' => $log->id, 'to_email' => $log->to, 'to_name' => $log->customer->fullname ?? null, 'customer_id' => $log->customer_id, 'subject' => $log->subject, 'status' => $log->status, 'created_at' => $log->created_at, ]); return response()->json([ 'error' => false, 'data' => [ ...$this->mapCampaign($campaign), 'cc' => $campaign->cc, 'bcc' => $campaign->bcc, 'variables' => $campaign->variables ?? [], 'logs' => $logs, ], ]); } public function variableSuggestions(Request $request): JsonResponse { $data = $request->validate([ 'template_id' => 'required|integer|exists:template,id', ]); $suggestions = MailCampaign::query() ->where('template_id', $data['template_id']) ->whereNotNull('variables') ->where('sent_count', '>', 0) ->orderByDesc('created_at') ->limit(20) ->get(['uuid', 'name', 'variables', 'created_at']) ->filter(fn (MailCampaign $campaign) => !empty($campaign->variables)) ->unique(fn (MailCampaign $campaign) => json_encode($campaign->variables)) ->take(8) ->map(fn (MailCampaign $campaign) => [ 'uuid' => $campaign->uuid, 'name' => $campaign->name, 'variables' => $campaign->variables, 'created_at' => $campaign->created_at, ]) ->values(); return response()->json([ 'error' => false, 'data' => $suggestions, ]); } public function preview(Request $request, MailCampaignRenderer $renderer): JsonResponse { $campaign = $this->draftCampaign($request->validate($this->contentRules())); return response()->json([ 'error' => false, 'data' => ['html' => $renderer->render($campaign, $renderer->sampleValues($campaign))['html']], ]); } public function previewCampaign(string $uuid, MailCampaignRenderer $renderer): JsonResponse { $campaign = MailCampaign::where('uuid', $uuid)->first(); if (!$campaign) { return response()->json(['error' => true, 'message' => 'Không tìm thấy chiến dịch'], 404); } return response()->json([ 'error' => false, 'data' => ['html' => $renderer->render($campaign, $renderer->sampleValues($campaign))['html']], ]); } public function showLog(string $uuid, int $id): JsonResponse { $log = Mail::query() ->whereKey($id) ->whereHas('campaign', fn ($q) => $q->where('uuid', $uuid)) ->first(); if (!$log) { return response()->json(['error' => true, 'message' => 'Không tìm thấy email'], 404); } return response()->json([ 'error' => false, 'data' => [ 'id' => $log->id, 'to_email' => $log->to, 'cc' => $log->cc, 'subject' => $log->subject, 'html' => $log->message, 'status' => $log->status, 'created_at' => $log->created_at, ], ]); } public function store(Request $request): JsonResponse { $data = $request->validate([ ...$this->contentRules(), 'recipient_type' => 'required|in:all,selected', 'uuids' => 'nullable|array', 'uuids.*' => 'string|exists:customers,uuid', 'emails' => 'nullable|array|max:1000', 'emails.*' => 'email|max:255', 'name' => 'nullable|string|max:255', 'cc' => 'nullable|string|max:1000', 'bcc' => 'nullable|string|max:1000', ]); $campaign = $this->draftCampaign($data); $recipients = $data['recipient_type'] === 'all' ? $this->allCustomerRecipients() : $this->selectedRecipients($data['uuids'] ?? [], $data['emails'] ?? []); if ($recipients === []) { return response()->json([ 'error' => true, 'message' => 'Không có người nhận hợp lệ để gửi.', ], 422); } $admin = Auth::guard('admin')->user(); $campaign->fill([ 'name' => trim((string) ($data['name'] ?? '')) ?: $campaign->subject, 'cc' => $data['cc'] ?? null, 'bcc' => $data['bcc'] ?? null, 'recipient_type' => $data['recipient_type'], 'total_recipients' => count($recipients), 'sent_count' => 0, 'failed_count' => 0, 'status' => 'queued', 'created_by' => $admin->id ?? null, ])->save(); foreach ($recipients as $email => $customerId) { SendMailCampaignRecipient::dispatch($campaign->id, $customerId, $email); } return response()->json([ 'error' => false, 'message' => "Đã đưa {$campaign->total_recipients} email vào hàng đợi gửi.", 'data' => $this->mapCampaign($campaign), ]); } private function contentRules(): array { return [ 'template_id' => 'nullable|integer|exists:template,id', 'subject' => 'required_without:template_id|nullable|string|max:255', 'message' => 'required_without:template_id|nullable|string', 'variables' => 'nullable|array', 'variables.*' => 'nullable|string|max:5000', ]; } private function draftCampaign(array $data): MailCampaign { $subject = trim((string) ($data['subject'] ?? '')); $message = trim((string) ($data['message'] ?? '')); $template = null; if (!empty($data['template_id'])) { $template = Template::query()->whereKey($data['template_id'])->where('status', 1)->first(); if (!$template || $template->format === 'json') { throw ValidationException::withMessages(['template_id' => 'Template không tồn tại, đang bị tắt hoặc không phải email HTML.']); } $subject = $subject !== '' ? $subject : trim((string) $template->subject); $message = trim((string) $template->message); } if ($subject === '' || $message === '') { throw ValidationException::withMessages(['subject' => 'Subject và content không được để trống.']); } $input = $data['variables'] ?? []; $declared = $template?->variables() ?? []; $variables = []; foreach (MailPlaceholders::extract($subject, $message) as $key) { $value = trim((string) ($input[$key] ?? '')); if ($value === '' && !array_key_exists($key, MailPlaceholders::RECIPIENT_FIELDS)) { $value = (string) ($declared[$key]['example'] ?? ''); } if ($value !== '') { $variables[$key] = $value; } } return new MailCampaign([ 'template_id' => $template?->id, 'subject' => $subject, 'message' => $message, 'variables' => $variables ?: null, ]); } /** * @return array */ private function allCustomerRecipients(): array { return Customer::query() ->whereNotNull('email') ->where('email', '!=', '') ->pluck('id', 'email') ->all(); } /** * @param array $uuids * @param array $emails * @return array */ private function selectedRecipients(array $uuids, array $emails): array { $emails = array_values(array_unique(array_map(fn ($email) => mb_strtolower(trim($email)), $emails))); $customers = Customer::query() ->where(fn ($q) => $q->whereIn('uuid', $uuids)->orWhereIn('email', $emails)) ->whereNotNull('email') ->get(['id', 'email', 'uuid']); $recipients = []; foreach ($customers as $customer) { if (in_array($customer->uuid, $uuids, true) || in_array(mb_strtolower($customer->email), $emails, true)) { $recipients[mb_strtolower($customer->email)] = $customer->id; } } foreach ($emails as $email) { $recipients[$email] ??= null; } return array_filter($recipients, fn ($id, $email) => filter_var($email, FILTER_VALIDATE_EMAIL), ARRAY_FILTER_USE_BOTH); } private function mapCampaign(MailCampaign $campaign): array { return [ 'id' => $campaign->id, 'uuid' => $campaign->uuid, 'name' => $campaign->name, 'subject' => $campaign->subject, 'template_id' => $campaign->template_id, 'template_name' => $campaign->template->name ?? null, 'total_recipients' => $campaign->total_recipients, 'sent_count' => $campaign->sent_count, 'failed_count' => $campaign->failed_count, 'status' => $campaign->status, 'recipient_type' => $campaign->recipient_type, 'created_by_name' => $campaign->creator->fullname ?? $campaign->creator->username ?? null, 'completed_at' => $campaign->completed_at, 'created_at' => $campaign->created_at, ]; } }