HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/hi.naniguide.com/app/Http/Controllers/Customer/EmailListController.php
<?php

namespace App\Http\Controllers\Customer;

use App\Http\Controllers\Controller;
use App\Models\Campaign;
use App\Models\EmailList;
use App\Models\ListSegment;
use App\Models\ListSubscriber;
use App\Services\EmailListService;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;

class EmailListController extends Controller
{
    public function __construct(
        protected EmailListService $emailListService
    ) {
        $this->middleware('customer.access:lists.permissions.can_access_lists')->only(['index', 'show']);
        $this->middleware('customer.access:lists.permissions.can_create_lists')->only(['create', 'store']);
        $this->middleware('customer.access:lists.permissions.can_edit_lists')->only(['edit', 'update', 'storeTag', 'updateTag', 'destroyTag']);
        $this->middleware('customer.access:lists.permissions.can_delete_lists')->only(['destroy']);
    }

    protected function authorizeOwnership(EmailList $list): EmailList
    {
        $customerId = auth('customer')->id();

        if (!$customerId || (int) $list->customer_id !== (int) $customerId) {
            abort(404);
        }

        return $list;
    }

    /**
     * Display a listing of the resource.
     */
    public function index(Request $request)
    {
        $viewTab = (string) $request->input('view', 'all');
        if ((string) $request->input('tab') === 'tags') {
            return redirect()->route('customer.tags.index');
        }
        $search  = trim((string) $request->input('search', ''));
        $sortBy  = (string) $request->input('sort', 'updated_at_desc');
        $perPage = 15;
        $customer = auth('customer')->user();

        // ── Audience stats (always computed) ───────────────────────────────
        $audienceRow = $this->workspaceResourceQuery(EmailList::query(), $customer)
            ->selectRaw('COUNT(*) as lists_count')
            ->first();

        $campaignRow = $this->workspaceResourceQuery(Campaign::query(), $customer)
            ->whereNotNull('list_id')
            ->selectRaw('COALESCE(SUM(CASE WHEN sent_count > bounced_count THEN sent_count - bounced_count ELSE 0 END), 0) as delivered_count')
            ->selectRaw('COALESCE(SUM(opened_count), 0) as opened_count')
            ->first();

        $deliveredTotal = (int) ($campaignRow->delivered_count ?? 0);
        $avgOpenRate    = $deliveredTotal > 0
            ? round((int) ($campaignRow->opened_count ?? 0) / $deliveredTotal * 100, 1)
            : 0.0;

        $listIds = $this->workspaceResourceQuery(EmailList::query(), $customer)->pluck('id')->all();

        $listsTotal    = (int) ($audienceRow->lists_count ?? 0);
        $segmentsTotal = ListSegment::query()
            ->whereHas('emailList', fn ($q) => $this->workspaceResourceQuery($q, $customer))
            ->count();

        $subscriberBaseQuery = ListSubscriber::query()->whereIn('list_id', $listIds);
        $totalContactsCount = (clone $subscriberBaseQuery)->count();
        $activeSubscribersCount = (clone $subscriberBaseQuery)->where('status', 'confirmed')->count();
        $unsubscribedCount = (clone $subscriberBaseQuery)->where('status', 'unsubscribed')->count();

        // ── Growth deltas (last 30 days vs previous 30 days) ──────────────────
        $delta = static function (int $curr, int $prev): ?float {
            if ($prev === 0) return null;
            return round(($curr - $prev) / $prev * 100, 1);
        };

        $currSubs = ListSubscriber::whereIn('list_id', $listIds)
            ->where('subscribed_at', '>=', now()->subDays(30))->count();
        $prevSubs = ListSubscriber::whereIn('list_id', $listIds)
            ->whereBetween('subscribed_at', [now()->subDays(60), now()->subDays(30)])->count();

        $currConfirmed = ListSubscriber::whereIn('list_id', $listIds)
            ->where('status', 'confirmed')->where('subscribed_at', '>=', now()->subDays(30))->count();
        $prevConfirmed = ListSubscriber::whereIn('list_id', $listIds)
            ->where('status', 'confirmed')
            ->whereBetween('subscribed_at', [now()->subDays(60), now()->subDays(30)])->count();

        $currUnsub = ListSubscriber::whereIn('list_id', $listIds)
            ->where('status', 'unsubscribed')->where('unsubscribed_at', '>=', now()->subDays(30))->count();
        $prevUnsub = ListSubscriber::whereIn('list_id', $listIds)
            ->where('status', 'unsubscribed')
            ->whereBetween('unsubscribed_at', [now()->subDays(60), now()->subDays(30)])->count();

        $audienceStats = [
            'total_contacts'         => $totalContactsCount,
            'total_contacts_delta'   => $delta($currSubs, $prevSubs),
            'active_subscribers'     => $activeSubscribersCount,
            'active_subscribers_delta' => $delta($currConfirmed, $prevConfirmed),
            'avg_open_rate'          => $avgOpenRate,
            'avg_open_rate_delta'    => null,
            'unsubscribed'           => $unsubscribedCount,
            'unsubscribed_delta'     => $delta($currUnsub, $prevUnsub),
            'lists_count'            => $listsTotal,
            'segments_count'         => $segmentsTotal,
            'all_count'              => $listsTotal + $segmentsTotal,
        ];

        // ── Sort config ─────────────────────────────────────────────────────
        [$sortCol, $sortDir] = match ($sortBy) {
            'name_asc'  => ['name', 'asc'],
            'name_desc' => ['name', 'desc'],
            'subs_desc' => ['subscribers_count', 'desc'],
            default     => ['updated_at', 'desc'],
        };

        // ── Build paginated table items ─────────────────────────────────────
        if ($viewTab === 'lists') {
            $listsQ = $this->workspaceResourceQuery(EmailList::query(), $customer)
                ->when($search, fn ($q) => $q->where(fn ($q2) =>
                    $q2->where('display_name', 'like', "%{$search}%")
                       ->orWhere('name', 'like', "%{$search}%")
                ))
                ->orderBy($sortCol === 'name' ? 'display_name' : $sortCol, $sortDir);

            $paginatedLists = $listsQ->paginate($perPage)->withQueryString();
            $perfMap = $this->listPerformanceMap((int) $customer->id, $paginatedLists->pluck('id')->all());
            $this->appendPerformanceToLists($paginatedLists, $perfMap);
            $paginator = $paginatedLists->setCollection(
                $paginatedLists->getCollection()->map(fn ($l) => $this->wrapListItem($l))
            );

        } elseif ($viewTab === 'segments') {
            $segsQ = ListSegment::query()
                ->whereHas('emailList', fn ($q) => $this->workspaceResourceQuery($q, $customer))
                ->with('emailList')
                ->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"))
                ->orderBy($sortCol, $sortDir);

            $paginatedSegs = $segsQ->paginate($perPage)->withQueryString();
            $paginator = $paginatedSegs->setCollection(
                $paginatedSegs->getCollection()->map(fn ($s) => $this->wrapSegmentItem($s))
            );

        } else {
            // all: load + merge + manual paginate
            $allLists = $this->workspaceResourceQuery(EmailList::query(), $customer)
                ->when($search, fn ($q) => $q->where(fn ($q2) =>
                    $q2->where('display_name', 'like', "%{$search}%")
                       ->orWhere('name', 'like', "%{$search}%")
                ))
                ->get();

            $perfMap = $this->listPerformanceMap((int) $customer->id, $allLists->pluck('id')->all());
            $allLists->each(function ($l) use ($perfMap) {
                $m = $perfMap[(int) $l->id] ?? ['open_rate' => 0.0, 'click_rate' => 0.0];
                $l->setAttribute('open_rate', $m['open_rate']);
                $l->setAttribute('click_rate', $m['click_rate']);
            });

            $allSegs = ListSegment::query()
                ->whereHas('emailList', fn ($q) => $this->workspaceResourceQuery($q, $customer))
                ->with('emailList')
                ->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"))
                ->get();

            $merged = collect()
                ->merge($allLists->map(fn ($l) => $this->wrapListItem($l)))
                ->merge($allSegs->map(fn ($s) => $this->wrapSegmentItem($s)));

            $merged = $sortDir === 'asc'
                ? $merged->sortBy($sortCol === 'name' ? 'name' : 'sort_key')->values()
                : $merged->sortByDesc($sortCol === 'name' ? 'name' : 'sort_key')->values();

            $currentPage = LengthAwarePaginator::resolveCurrentPage();
            $paginator   = new LengthAwarePaginator(
                $merged->slice(($currentPage - 1) * $perPage, $perPage)->values(),
                $merged->count(),
                $perPage,
                $currentPage,
                ['path' => $request->url(), 'query' => $request->query()]
            );
        }

        return view('customer.lists.index', compact('audienceStats', 'viewTab', 'search', 'sortBy', 'paginator'));
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create()
    {
        return view('customer.lists.create');
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request)
    {
        $customer = auth('customer')->user();
        $customer->enforceGroupLimit('lists.limits.max_lists', $customer->emailLists()->count(), 'Email list limit reached.');

        $validated = $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'display_name' => ['nullable', 'string', 'max:255'],
            'description' => ['nullable', 'string'],
            'from_name' => ['nullable', 'string', 'max:255'],
            'from_email' => ['nullable', 'email', 'max:255'],
            'reply_to' => ['nullable', 'email', 'max:255'],
            'status' => ['nullable', 'in:active,inactive,pending'],
            'opt_in' => ['nullable', 'in:single,double'],
            'opt_out' => ['nullable', 'in:single,double'],
            'double_opt_in' => ['nullable', 'boolean'],
            'default_subject' => ['nullable', 'string', 'max:255'],
            'company_name' => ['nullable', 'string', 'max:255'],
            'company_address' => ['nullable', 'string'],
            'footer_text' => ['nullable', 'string'],
            'welcome_email_enabled' => ['nullable', 'boolean'],
            'welcome_email_subject' => ['nullable', 'string', 'max:255'],
            'welcome_email_content' => ['nullable', 'string'],
            'unsubscribe_email_enabled' => ['nullable', 'boolean'],
            'unsubscribe_email_subject' => ['nullable', 'string', 'max:255'],
            'unsubscribe_email_content' => ['nullable', 'string'],
            'tags' => ['nullable'],
        ]);

        $validated['tags'] = $this->normalizeTags($request->input('tags'));
        $validated = $this->syncOptInFields($validated);

        $emailList = $this->emailListService->create($customer, $validated);

        return redirect()
            ->route('customer.lists.show', $emailList)
            ->with('success', 'Email list created successfully.');
    }

    /**
     * Display the specified resource.
     */
    public function show(EmailList $list)
    {
        $this->authorizeOwnership($list);

        $performance = $this->listPerformanceMap((int) $list->customer_id, [(int) $list->id]);
        $listPerformance = $performance[(int) $list->id] ?? [
            'campaigns_count' => 0,
            'sent_count' => 0,
            'delivered_count' => 0,
            'opened_count' => 0,
            'clicked_count' => 0,
            'open_rate' => 0.0,
            'click_rate' => 0.0,
        ];

        $recentCampaigns = Campaign::query()
            ->where('customer_id', (int) $list->customer_id)
            ->where('list_id', (int) $list->id)
            ->latest('created_at')
            ->limit(10)
            ->get(['id', 'name', 'status', 'sent_count', 'opened_count', 'clicked_count', 'bounced_count', 'created_at']);

        $recentAdditions = $list->subscribers()
            ->latest('subscribed_at')
            ->limit(5)
            ->get(['id', 'email', 'first_name', 'last_name', 'source', 'subscribed_at']);

        $growthPeriods = $this->buildGrowthData($list);

        $segmentsCount = $list->segments()->count();
        $confirmedSubscribersCount = $list->confirmedSubscribers()->count();

        [$healthScore, $healthLabel] = $this->computeHealthScore($list, $listPerformance);

        $subscriberDelta = $this->computeSubscriberDelta($list);

        return view('customer.lists.show', compact(
            'list', 'listPerformance', 'recentCampaigns',
            'recentAdditions', 'growthPeriods', 'segmentsCount',
            'healthScore', 'healthLabel', 'subscriberDelta',
            'confirmedSubscribersCount'
        ));
    }

    private function buildGrowthData(EmailList $list): array
    {
        $periods = [7, 30, 90];
        $result = [];

        foreach ($periods as $days) {
            $rows = $list->subscribers()
                ->selectRaw('DATE(subscribed_at) as date, COUNT(*) as cnt')
                ->where('subscribed_at', '>=', now()->subDays($days - 1)->startOfDay())
                ->groupByRaw('DATE(subscribed_at)')
                ->orderBy('date')
                ->pluck('cnt', 'date')
                ->toArray();

            $labels = [];
            $values = [];
            for ($i = $days - 1; $i >= 0; $i--) {
                $date = now()->subDays($i)->format('Y-m-d');
                $labels[] = $days <= 7
                    ? now()->subDays($i)->format('D')
                    : now()->subDays($i)->format('M d');
                $values[] = (int) ($rows[$date] ?? 0);
            }

            $result[$days] = ['labels' => $labels, 'values' => $values];
        }

        return $result;
    }

    private function computeHealthScore(EmailList $list, array $performance): array
    {
        $total = max(1, (int) $list->subscribers_count);
        $confirmedRate = min(100, ((int) $list->confirmed_subscribers_count / $total) * 100);
        $bounceRate    = ((int) $list->bounced_count / $total) * 100;
        $openRate      = min(100, (float) ($performance['open_rate'] ?? 0));
        $clickRate     = min(100, (float) ($performance['click_rate'] ?? 0));

        $score = (int) round(
            ($confirmedRate * 0.30) +
            (min($openRate, 60) / 60 * 100 * 0.30) +
            (min($clickRate, 20) / 20 * 100 * 0.20) +
            (max(0.0, 100.0 - $bounceRate * 5) * 0.20)
        );
        $score = max(0, min(100, $score));

        $label = match (true) {
            $score >= 90 => 'Excellent',
            $score >= 70 => 'Good',
            $score >= 50 => 'Fair',
            default      => 'Poor',
        };

        return [$score, $label];
    }

    private function computeSubscriberDelta(EmailList $list): float
    {
        $current  = $list->subscribers()->where('subscribed_at', '>=', now()->subDays(30))->count();
        $previous = $list->subscribers()
            ->where('subscribed_at', '>=', now()->subDays(60))
            ->where('subscribed_at', '<', now()->subDays(30))
            ->count();

        if ($previous === 0) {
            return $current > 0 ? 100.0 : 0.0;
        }

        return round(($current - $previous) / $previous * 100, 1);
    }

    private function listPerformanceMap(int $customerId, array $listIds): array
    {
        $listIds = array_values(array_filter(array_map('intval', $listIds), static fn (int $id) => $id > 0));
        if ($listIds === []) {
            return [];
        }

        $rows = Campaign::query()
            ->where('customer_id', $customerId)
            ->whereIn('list_id', $listIds)
            ->groupBy('list_id')
            ->selectRaw('list_id')
            ->selectRaw('COUNT(*) as campaigns_count')
            ->selectRaw('COALESCE(SUM(sent_count), 0) as sent_count')
            ->selectRaw('COALESCE(SUM(CASE WHEN sent_count > bounced_count THEN sent_count - bounced_count ELSE 0 END), 0) as delivered_count')
            ->selectRaw('COALESCE(SUM(opened_count), 0) as opened_count')
            ->selectRaw('COALESCE(SUM(clicked_count), 0) as clicked_count')
            ->get();

        $map = [];
        foreach ($rows as $row) {
            $listId = (int) ($row->list_id ?? 0);
            if ($listId <= 0) {
                continue;
            }

            $delivered = (int) ($row->delivered_count ?? 0);
            $opened = (int) ($row->opened_count ?? 0);
            $clicked = (int) ($row->clicked_count ?? 0);

            $map[$listId] = [
                'campaigns_count' => (int) ($row->campaigns_count ?? 0),
                'sent_count' => (int) ($row->sent_count ?? 0),
                'delivered_count' => $delivered,
                'opened_count' => $opened,
                'clicked_count' => $clicked,
                'open_rate' => $delivered > 0 ? round(($opened / $delivered) * 100, 2) : 0.0,
                'click_rate' => $delivered > 0 ? round(($clicked / $delivered) * 100, 2) : 0.0,
            ];
        }

        return $map;
    }

    private function appendPerformanceToLists(LengthAwarePaginator $emailLists, array $performanceMap): void
    {
        $collection = $emailLists->getCollection();

        if (!$collection instanceof Collection) {
            return;
        }

        $collection->transform(function (EmailList $list) use ($performanceMap) {
            $metrics = $performanceMap[(int) $list->id] ?? [
                'campaigns_count' => 0,
                'sent_count' => 0,
                'delivered_count' => 0,
                'opened_count' => 0,
                'clicked_count' => 0,
                'open_rate' => 0.0,
                'click_rate' => 0.0,
            ];

            $list->setAttribute('campaigns_count', $metrics['campaigns_count']);
            $list->setAttribute('sent_count', $metrics['sent_count']);
            $list->setAttribute('delivered_count', $metrics['delivered_count']);
            $list->setAttribute('opened_count', $metrics['opened_count']);
            $list->setAttribute('clicked_count', $metrics['clicked_count']);
            $list->setAttribute('open_rate', $metrics['open_rate']);
            $list->setAttribute('click_rate', $metrics['click_rate']);

            return $list;
        });
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit(EmailList $list)
    {
        $this->authorizeOwnership($list);
        return view('customer.lists.edit', compact('list'));
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, EmailList $list)
    {
        $this->authorizeOwnership($list);
        $validated = $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'display_name' => ['nullable', 'string', 'max:255'],
            'description' => ['nullable', 'string'],
            'from_name' => ['nullable', 'string', 'max:255'],
            'from_email' => ['nullable', 'email', 'max:255'],
            'reply_to' => ['nullable', 'email', 'max:255'],
            'status' => ['nullable', 'in:active,inactive,pending'],
            'opt_in' => ['nullable', 'in:single,double'],
            'opt_out' => ['nullable', 'in:single,double'],
            'double_opt_in' => ['nullable', 'boolean'],
            'default_subject' => ['nullable', 'string', 'max:255'],
            'company_name' => ['nullable', 'string', 'max:255'],
            'company_address' => ['nullable', 'string'],
            'footer_text' => ['nullable', 'string'],
            'welcome_email_enabled' => ['nullable', 'boolean'],
            'welcome_email_subject' => ['nullable', 'string', 'max:255'],
            'welcome_email_content' => ['nullable', 'string'],
            'unsubscribe_email_enabled' => ['nullable', 'boolean'],
            'unsubscribe_email_subject' => ['nullable', 'string', 'max:255'],
            'unsubscribe_email_content' => ['nullable', 'string'],
            'tags' => ['nullable'],
        ]);

        $validated['tags'] = $this->normalizeTags($request->input('tags'));
        $validated = $this->syncOptInFields($validated);

        $this->emailListService->update($list, $validated);

        return redirect()
            ->route('customer.lists.show', $list)
            ->with('success', 'Email list updated successfully.');
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(EmailList $list)
    {
        $this->authorizeOwnership($list);
        $this->emailListService->delete($list);

        return redirect()
            ->route('customer.lists.index')
            ->with('success', 'Email list deleted successfully.');
    }

    public function storeTag(Request $request, EmailList $list)
    {
        $this->authorizeOwnership($list);

        $validated = $request->validate([
            'tag' => ['required', 'string', 'max:100'],
        ]);

        $existingTags = is_array($list->tags ?? null) ? $list->tags : [];
        $newTag = trim((string) $validated['tag']);

        if ($newTag !== '' && !in_array($newTag, $existingTags, true)) {
            $existingTags[] = $newTag;
        }

        $list->update([
            'tags' => $this->normalizeTags($existingTags),
        ]);

        return redirect()
            ->route('customer.tags.index')
            ->with('success', 'Tag added successfully.');
    }

    public function updateTag(Request $request, EmailList $list)
    {
        $this->authorizeOwnership($list);

        $validated = $request->validate([
            'old_tag' => ['required', 'string', 'max:100'],
            'new_tag' => ['required', 'string', 'max:100'],
        ]);

        $tags = is_array($list->tags ?? null) ? $list->tags : [];
        $oldTag = trim((string) $validated['old_tag']);
        $newTag = trim((string) $validated['new_tag']);

        foreach ($tags as $index => $tag) {
            if ((string) $tag === $oldTag) {
                $tags[$index] = $newTag;
            }
        }

        $list->update([
            'tags' => $this->normalizeTags($tags),
        ]);

        return redirect()
            ->route('customer.tags.index')
            ->with('success', 'Tag updated successfully.');
    }

    public function destroyTag(Request $request, EmailList $list)
    {
        $this->authorizeOwnership($list);

        $validated = $request->validate([
            'tag' => ['required', 'string', 'max:100'],
        ]);

        $tagToDelete = trim((string) $validated['tag']);
        $tags = is_array($list->tags ?? null) ? $list->tags : [];
        $tags = array_values(array_filter($tags, static fn ($tag) => (string) $tag !== $tagToDelete));

        $list->update([
            'tags' => $this->normalizeTags($tags),
        ]);

        return redirect()
            ->route('customer.tags.index')
            ->with('success', 'Tag deleted successfully.');
    }

    private function normalizeTags(mixed $tagsInput): array
    {
        $tags = [];

        if (is_string($tagsInput)) {
            $tags = preg_split('/[,\n]+/', $tagsInput) ?: [];
        } elseif (is_array($tagsInput)) {
            $tags = $tagsInput;
        }

        $normalized = array_values(array_unique(array_filter(array_map(
            static fn ($tag) => trim((string) $tag),
            $tags
        ), static fn (string $tag) => $tag !== '')));

        return array_slice($normalized, 0, 50);
    }

    private function syncOptInFields(array $data): array
    {
        if (array_key_exists('opt_in', $data)) {
            $data['double_opt_in'] = $data['opt_in'] === 'double';
        } elseif (array_key_exists('double_opt_in', $data)) {
            $data['opt_in'] = (bool) $data['double_opt_in'] ? 'double' : 'single';
        }

        return $data;
    }

    private function wrapListItem(EmailList $l): array
    {
        return [
            'type'       => 'list',
            'id'         => $l->id,
            'name'       => $l->display_name ?? $l->name,
            'subtitle'   => 'ID: ' . ($l->name ?? ''),
            'subscribers'=> (int) ($l->subscribers_count ?? 0),
            'open_rate'  => (float) ($l->open_rate ?? 0),
            'click_rate' => (float) ($l->click_rate ?? 0),
            'updated_at' => $l->updated_at,
            'sort_key'   => $l->updated_at?->timestamp ?? 0,
            'url'        => route('customer.lists.show', $l),
        ];
    }

    private function wrapSegmentItem(ListSegment $s): array
    {
        $rules     = is_array($s->rules) ? $s->rules : [];
        $condCount = is_array($rules['conditions'] ?? null) ? count($rules['conditions']) : 0;

        return [
            'type'       => 'segment',
            'id'         => $s->id,
            'list_id'    => $s->emailList?->id,
            'name'       => $s->name,
            'subtitle'   => 'Conditions: ' . $condCount . ' ' . ($condCount === 1 ? 'Rule' : 'Rules'),
            'subscribers'=> (int) ($s->subscribers_count ?? 0),
            'open_rate'  => 0.0,
            'click_rate' => 0.0,
            'updated_at' => $s->updated_at,
            'sort_key'   => $s->updated_at?->timestamp ?? 0,
            'url'        => $s->emailList ? route('customer.lists.subscribers.index', $s->emailList) . '?segment_id=' . $s->id : '#',
        ];
    }
}