Your IP : 216.73.216.79


Current Path : /var/www/html/reddsis/app/Services/
Upload File :
Current File : /var/www/html/reddsis/app/Services/PortalHandler.php

<?php

namespace App\Services;

use App\Http\Helpers\SettingHelper;
use App\Models\Backend\FrontendPage;
use App\Models\Backend\FrontendSite;
use App\Models\Backend\Setting;
use App\Models\Backend\Visit;
use App\Models\Backend\menu\frontend\Menu;
use App\Models\Backend\menu\frontend\MenuCategory;
use App\Models\Backend\menu\frontend\RoleMapping;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\ViewErrorBag;

class PortalHandler
{
    public function __construct(
        protected FrontendSiteCacheService $cache,
        protected BladeSyncService $sync
    ) {}

    /**
     * Main handler untuk render frontend site.
     *
     * 1. Ambil meta site dari cache
     * 2. Cari page (default atau ikut slug)
     * 3. Ambil menu assignments untuk role public
     * 4. Build menu tree (parent-child)
     * 5. Render entry script → components → page layout → header/footer → site layout
     *
     * Return HTML string siap render.
     */
    public function handle(string $siteSlug, ?string $pageSlug = null): string
    {
        $meta = $this->cache->get($siteSlug);
        abort_unless($meta, 404);

        $site = FrontendSite::where('slug', $siteSlug)->first();

        if (!$site) {
            abort(404);
        }

        if (!$site->status) {
            $siteName = $meta['name'] ?? $siteSlug;
            return view('frontend.empty-site', [
                'siteName' => $siteName,
                'siteSlug' => $siteSlug,
                'state' => 'site-inactive',
            ])->render();
        }

        if ($pageSlug) {
            $page = FrontendPage::where('site_fk', $site->id)->where('slug', $pageSlug)->where('status', true)->first();
        } else {
            $page = $site->defaultPage()->where('status', true)->first();
        }

        if (!$page) {
            $siteName = $meta['name'] ?? $siteSlug;
            $hasPages = FrontendPage::where('site_fk', $site->id)->exists();
            $state = $hasPages ? 'pages-inactive' : 'no-pages';
            return view('frontend.empty-site', compact('siteName', 'siteSlug', 'state'))->render();
        }

        // Senarai semua pages dalam site (guna untuk navbar fallback)
        $sitePages = $meta['pages'] ?? [];
        $pageList = collect($sitePages)->map(fn($p, $slug) => [
            'slug' => $slug,
            'name' => $p['name'] ?? $slug,
            'url' => "/{$slug}",
        ])->values();

        // Ambil menu assignments untuk public kat site ni (navbar category)
        $navbarCategoryId = MenuCategory::where('category_name', 'Navbar Public')->value('category_id')
            ?? MenuCategory::where('category_name', 'main-navbar')->value('category_id');

        $publicMappings = RoleMapping::with(['menu', 'category'])
            ->where('role_code', 'public')
            ->where('category_id', $navbarCategoryId)
            ->where(function ($q) use ($site) {
                $q->where('site_id', $site->id)
                  ->orWhere('site_id', 0);
            })
            ->where('status', true)
            ->orderBy('sort')
            ->get()
            ->filter(fn ($m) => $m->menu && $m->menu->menu_status)
            ->values();

        // Build nested menu tree berdasarkan parent_id → menu_id hierarchy
        $buildTree = function($items, $parentId = 0) use (&$buildTree) {
            $tree = [];
            foreach ($items as $item) {
                if ((int) $item->parent_id === $parentId) {
                    $itemArray = $item->menu->toArray();
                    $children = $buildTree($items, (int) $item->menu_id);
                    if ($children) $itemArray['children'] = $children;
                    $tree[] = $itemArray;
                }
            }
            return $tree;
        };
        $translateMenuName = function ($name) {
            return t($name);
        };

        $menuTree = $buildTree($publicMappings);

        // Translate menu names
        array_walk_recursive($menuTree, function (&$v, $k) use ($translateMenuName) {
            if ($k === 'menu_name') $v = $translateMenuName($v);
        });

        // Fallback: guna semua page site sebagai menu rata (flat)
        if ($publicMappings->isEmpty()) {
            $menuTree = $pageList->map(fn ($p) => [
                'menu_name' => $translateMenuName($p['name']),
                'menu_link' => $p['url'],
                'menu_icon' => null,
                'children' => [],
            ])->toArray();
            $allMenus = $pageList->map(fn ($p) => (object) [
                'menu_name' => $translateMenuName($p['name']),
                'menu_link' => $p['url'],
                'menu_icon' => null,
            ]);
        } else {
            $allMenus = $publicMappings->map(fn ($m) => $m->menu);
        }

        // Translate allMenus
        $allMenus = $allMenus->map(function ($m) use ($translateMenuName) {
            if (isset($m->menu_name)) $m->menu_name = $translateMenuName($m->menu_name);
            return $m;
        });

        // Footer menu — category "Footer Links"
        $footerCategoryId = MenuCategory::where('category_name', 'Footer Links')->value('category_id');
        $footerMappings = RoleMapping::with(['menu'])
            ->where('role_code', 'public')
            ->where('category_id', $footerCategoryId)
            ->where(function ($q) use ($site) {
                $q->where('site_id', $site->id)->orWhere('site_id', 0);
            })
            ->where('status', true)
            ->orderBy('sort')
            ->get()
            ->filter(fn ($m) => $m->menu && $m->menu->menu_status)
            ->values();
        $footerMenus = $footerMappings->map(fn ($m) => $m->menu);
        if ($footerMappings->isEmpty()) {
            $footerMenus = collect();
        }

        // Translate footer menu names
        $footerMenus = $footerMenus->map(function ($m) use ($translateMenuName) {
            if (isset($m->menu_name)) $m->menu_name = $translateMenuName($m->menu_name);
            return $m;
        });

        // Visitor stats
        $totalVisits = Visit::totalVisits();
        $todayVisits = Visit::todayVisits();
        $uniqueIps = Visit::uniqueIps();

        // Variable untuk passed ke blade components
        $_favicon = SettingHelper::get('favicon', 'favicon.ico');
        $_faviconUrl = (!str_starts_with($_favicon, 'dynaweb4/') && !str_starts_with($_favicon, 'favicon'))
            ? Storage::url($_favicon)
            : asset($_favicon);
        $_faviconVer = Setting::where('key', 'favicon')->value('updated_at')?->timestamp ?? 0;

        $lang = session('locale', 'ms');

        $vars = [
            'lang' => $lang,
            'siteSlug' => $siteSlug,
            'siteName' => $meta['name'] ?? '',
            'sitePages' => $pageList,
            'menus' => $allMenus,
            'menuTree' => $menuTree,
            'footerMenus' => $footerMenus,
            'totalVisits' => $totalVisits,
            'todayVisits' => $todayVisits,
            'uniqueIps' => $uniqueIps,
            '_faviconUrl' => "{$_faviconUrl}?v={$_faviconVer}",
        ];

        // Shared object — supaya @php variable dalam entry script boleh dikongsi
        // Guna $_shared->nama = 'value' dalam entry script,
        // then $_shared->nama boleh diguna di component/pageContent/layout
        $shared = new \stdClass();
        $vars['_shared'] = $shared;

        // Entry Script — init preprocessing (run first, wajib load)
        // Variables declared with @php $var = value in the entry script are
        // automatically captured and forwarded to components/page content/layout.
        $entryOutput = '';
        $entryBlade = Storage::get("ENTRY/PAGE/{$siteSlug}/{$page->slug}.blade.php");
        if ($entryBlade) {
            // Validate: entry_script tidak boleh mengandungi {!! !!} syntax
            if (preg_match('/\{!!\s*\$/', $entryBlade)) {
                $errorMsg = "Entry Script for page '{$page->slug}' contains {!! \$... !!} syntax. "
                    . "Entry Script is for @php logic only. Move component output to Page Content.";
                \Illuminate\Support\Facades\Log::error($errorMsg);
                abort(500, $errorMsg);
            }
            $compiled = Blade::compileString($entryBlade);
            $__entryTmp = tempnam(sys_get_temp_dir(), 'opencode_entry') . '.php';
            file_put_contents($__entryTmp, $compiled);

            $__entryResult = (function () use ($__entryTmp, $vars) {
                $__origKeys = array_keys($vars);
                extract($vars);
                ob_start();
                include $__entryTmp;
                $output = ob_get_clean();
                $all = get_defined_vars();

                $new = [];
                foreach ($all as $k => $v) {
                    if (!in_array($k, $__origKeys, true) && !in_array($k, [
                        '__origKeys', 'vars', 'output', 'all', 'new', 'k', 'v', '__entryTmp'
                    ], true)) {
                        $new[$k] = $v;
                    }
                }
                return ['output' => $output, 'newVars' => $new];
            })();

            $entryOutput = $__entryResult['output'];
            $vars = array_merge($vars, $__entryResult['newVars']);
            unlink($__entryTmp);
        }
        $vars['__entry__'] = $entryOutput;

        // Render setiap component page (hero, features, etc)
        foreach ($page->components as $comp) {
            $compCode = $comp->component ? $comp->component->code : $comp->code;
            $blade = Storage::get("PHP/{$compCode}.blade.php");
            if ($blade) {
                try {
                    $vars[$comp->code] = Blade::render($blade, $vars);
                } catch (\Throwable $e) {
                    $vars[$comp->code] = '<div style="padding:16px;margin:8px 0;border:2px solid #ef4444;border-radius:8px;background:#fef2f2;font-family:sans-serif;font-size:13px;">
                        <strong style="color:#dc2626;">⚠ Component Error: ' . e($comp->name) . ' ($' . e($comp->code) . ')</strong>
                        <p style="margin:4px 0 0;color:#991b1b;">' . e($e->getMessage()) . '</p>
                    </div>';
                }
            } else {
                $vars[$comp->code] = '';
            }
        }

        // Render Page Content
        $pageHtml = '';
        $pageContentBlade = Storage::get("PAGE_CONTENT/{$siteSlug}/{$page->slug}.blade.php");
        if ($pageContentBlade) {
            try {
                $pageHtml = Blade::render($pageContentBlade, $vars);
            } catch (\Throwable $e) {
                $pageHtml = '<div style="padding:16px;margin:8px 0;border:2px solid #ef4444;border-radius:8px;background:#fef2f2;font-family:sans-serif;font-size:13px;">
                    <strong style="color:#dc2626;">⚠ Page Content Error</strong>
                    <p style="margin:4px 0 0;color:#991b1b;">' . e($e->getMessage()) . '</p>
                </div>';
            }
        } elseif ($entryOutput) {
            $pageHtml = $entryOutput;
        }

        // Render header
        $header = '';
        if ($site->header_fk && $site->header) {
            $headerBlade = Storage::get("PHP/{$site->header->code}.blade.php");
            if ($headerBlade) {
                try {
                    $header = Blade::render($headerBlade, $vars);
                } catch (\Throwable $e) {
                    $header = '<div style="padding:12px;border:2px solid #ef4444;border-radius:8px;margin:4px;background:#fef2f2;font-family:sans-serif;font-size:13px;">
                        <strong style="color:#dc2626;">⚠ Header Error (' . e($site->header->code) . ')</strong>
                        <p style="margin:4px 0 0;color:#991b1b;">' . e($e->getMessage()) . '</p>
                    </div>';
                }
            }
        }

        // Render footer
        $footer = '';
        if ($site->footer_fk && $site->footer) {
            $footerBlade = Storage::get("PHP/{$site->footer->code}.blade.php");
            if ($footerBlade) {
                try {
                    $footer = Blade::render($footerBlade, $vars);
                } catch (\Throwable $e) {
                    $footer = '<div style="padding:12px;border:2px solid #ef4444;border-radius:8px;margin:4px;background:#fef2f2;font-family:sans-serif;font-size:13px;">
                        <strong style="color:#dc2626;">⚠ Footer Error (' . e($site->footer->code) . ')</strong>
                        <p style="margin:4px 0 0;color:#991b1b;">' . e($e->getMessage()) . '</p>
                    </div>';
                }
            }
        }

        // Page layout → override site layout kalau use_custom_layout enabled
        $siteLayout = Storage::get("LAYOUTS/SITE/{$site->slug}.blade.php");
        $layoutVars = array_merge($vars, [
            '__header__' => $header,
            '__page__' => $pageHtml,
            '__footer__' => $footer,
            'errors' => session('errors') ?: new ViewErrorBag,
        ]);

        try {
            if ($page->use_custom_layout) {
                // If a dedicated PAGE layout exists, use it so the page can
                // keep/replace header/footer as needed while still rendering
                // the shared page content as __page__.
                $pageLayout = Storage::get("LAYOUTS/PAGE/{$siteSlug}/{$page->slug}.blade.php");
                if ($pageLayout) {
                    $html = Blade::render($pageLayout, $layoutVars);
                } elseif ($pageContentBlade) {
                    // Fully standalone page content: render directly.
                    $html = $pageHtml;
                } elseif ($siteLayout) {
                    $html = Blade::render($siteLayout, $layoutVars);
                } else {
                    $html = $pageHtml;
                }
            } elseif ($siteLayout) {
                $html = Blade::render($siteLayout, $layoutVars);
            } else {
                $html = $pageHtml;
            }
        } catch (HttpResponseException $e) {
            throw $e;
        } catch (\Throwable $e) {
            $html = '<div style="max-width:800px;margin:40px auto;padding:24px;border:2px solid #ef4444;border-radius:12px;background:#fef2f2;font-family:sans-serif;">
                <h2 style="color:#dc2626;margin:0 0 8px;">⚠ Layout Rendering Error</h2>
                <p style="color:#991b1b;margin:0 0 12px;">' . e($e->getMessage()) . '</p>
                <hr style="border:none;border-top:1px solid #fca5a5;margin:12px 0;">
                <p style="font-size:12px;color:#b91c1c;">Site: ' . e($site->name) . ' | Page: ' . e($page->name) . '</p>
            </div>';
        }

        return $html;
    }

}