chore: initial Laravel 13 project setup for eCert MBIP

- Laravel 13.9 + PHP 8.5 + MySQL
- Bootstrap 5.3 + jQuery 3.7 + Chart.js (replacing Alpine/Tailwind)
- Packages: intervention/image, dompdf, simple-qrcode, league/csv, laravel/breeze, laravel/boost
- 17 database migrations: users, programs, qr_codes, participants, attendances, certificates, questionnaires, email_logs, audit_logs
- 13 Eloquent models with full relationships
- Admin layout (Bootstrap 5 sidebar) + public layout (mobile-first)
- Rate limiters: checkin (60/min), certificate (30/min)
- Admin seeder: admin@mbip.gov.my
- Storage directories + symlink configured

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Saufi
2026-05-16 15:44:19 +08:00
commit 5b85822b78
159 changed files with 18351 additions and 0 deletions

View File

@@ -0,0 +1,154 @@
<!DOCTYPE html>
<html lang="ms">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>@yield('title', 'eCert MBIP') Admin</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
@stack('styles')
</head>
<body>
<div class="d-flex">
{{-- Sidebar --}}
<nav class="sidebar d-none d-md-flex flex-column" style="min-height:100vh; width:260px; flex-shrink:0;">
<div class="sidebar-brand">
<h5><i class="bi bi-award-fill me-2"></i>eCert MBIP</h5>
<small>Sistem Pengurusan Sijil Digital</small>
</div>
<ul class="nav flex-column mt-2 px-1 flex-grow-1">
<li class="nav-item">
<a href="{{ route('admin.dashboard') }}"
class="nav-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
<i class="bi bi-grid-1x2-fill"></i> Dashboard
</a>
</li>
<li class="nav-item mt-2">
<small class="text-white-50 px-3" style="font-size:.7rem; text-transform:uppercase; letter-spacing:.8px;">Program</small>
</li>
<li class="nav-item">
<a href="{{ route('admin.programs.index') }}"
class="nav-link {{ request()->routeIs('admin.programs.*') ? 'active' : '' }}">
<i class="bi bi-calendar-event-fill"></i> Senarai Program
</a>
</li>
<li class="nav-item mt-2">
<small class="text-white-50 px-3" style="font-size:.7rem; text-transform:uppercase; letter-spacing:.8px;">Soalselidik</small>
</li>
<li class="nav-item">
<a href="{{ route('admin.questionnaires.index') }}"
class="nav-link {{ request()->routeIs('admin.questionnaires.*') ? 'active' : '' }}">
<i class="bi bi-clipboard2-check-fill"></i> Set Soalselidik
</a>
</li>
</ul>
<div class="px-3 pb-3 mt-auto">
<div class="border-top border-white border-opacity-25 pt-3">
<small class="text-white-50 d-block mb-1">{{ auth()->user()->name }}</small>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="btn btn-sm btn-outline-light w-100">
<i class="bi bi-box-arrow-right me-1"></i> Log Keluar
</button>
</form>
</div>
</div>
</nav>
{{-- Main Content --}}
<div class="main-content flex-grow-1">
{{-- Top Navbar (mobile) --}}
<nav class="navbar navbar-light bg-white border-bottom d-md-none px-3">
<span class="navbar-brand fw-bold text-primary mb-0">
<i class="bi bi-award-fill me-1"></i>eCert MBIP
</span>
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="offcanvas" data-bs-target="#mobileSidebar">
<i class="bi bi-list"></i>
</button>
</nav>
{{-- Page Header --}}
@if (isset($header) || View::hasSection('header'))
<div class="page-header d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0 fw-semibold">@yield('header')</h5>
@if(View::hasSection('breadcrumb'))
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0 mt-1" style="font-size:.82rem;">
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
@yield('breadcrumb')
</ol>
</nav>
@endif
</div>
<div>@yield('header-actions')</div>
</div>
@endif
{{-- Flash Messages --}}
<div class="px-4 pt-3">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i>{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>{{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('warning'))
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-circle-fill me-2"></i>{{ session('warning') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
</div>
{{-- Page Content --}}
<div class="p-4">
@yield('content')
</div>
</div>
</div>
{{-- Mobile Offcanvas Sidebar --}}
<div class="offcanvas offcanvas-start" tabindex="-1" id="mobileSidebar" style="width:260px; background: var(--mbip-primary);">
<div class="offcanvas-header border-bottom border-white border-opacity-25">
<h6 class="text-white mb-0"><i class="bi bi-award-fill me-2"></i>eCert MBIP</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas"></button>
</div>
<div class="offcanvas-body p-0">
<ul class="nav flex-column mt-2 px-1">
<li class="nav-item">
<a href="{{ route('admin.dashboard') }}" class="nav-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}" style="color:rgba(255,255,255,.85);">
<i class="bi bi-grid-1x2-fill me-2"></i>Dashboard
</a>
</li>
<li class="nav-item">
<a href="{{ route('admin.programs.index') }}" class="nav-link {{ request()->routeIs('admin.programs.*') ? 'active' : '' }}" style="color:rgba(255,255,255,.85);">
<i class="bi bi-calendar-event-fill me-2"></i>Senarai Program
</a>
</li>
<li class="nav-item">
<a href="{{ route('admin.questionnaires.index') }}" class="nav-link {{ request()->routeIs('admin.questionnaires.*') ? 'active' : '' }}" style="color:rgba(255,255,255,.85);">
<i class="bi bi-clipboard2-check-fill me-2"></i>Set Soalselidik
</a>
</li>
</ul>
</div>
</div>
@stack('scripts')
</body>
</html>

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased">
<div class="min-h-screen bg-gray-100">
@include('layouts.navigation')
<!-- Page Heading -->
@isset($header)
<header class="bg-white shadow">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{{ $header }}
</div>
</header>
@endisset
<!-- Page Content -->
<main>
{{ $slot }}
</main>
</div>
</body>
</html>

View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans text-gray-900 antialiased">
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
<div>
<a href="/">
<x-application-logo class="w-20 h-20 fill-current text-gray-500" />
</a>
</div>
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
{{ $slot }}
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,100 @@
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
<!-- Primary Navigation Menu -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<!-- Logo -->
<div class="shrink-0 flex items-center">
<a href="{{ route('dashboard') }}">
<x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
</a>
</div>
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-nav-link>
</div>
</div>
<!-- Settings Dropdown -->
<div class="hidden sm:flex sm:items-center sm:ms-6">
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150">
<div>{{ Auth::user()->name }}</div>
<div class="ms-1">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</div>
</button>
</x-slot>
<x-slot name="content">
<x-dropdown-link :href="route('profile.edit')">
{{ __('Profile') }}
</x-dropdown-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-dropdown-link :href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-dropdown-link>
</form>
</x-slot>
</x-dropdown>
</div>
<!-- Hamburger -->
<div class="-me-2 flex items-center sm:hidden">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
<!-- Responsive Navigation Menu -->
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
<div class="pt-2 pb-3 space-y-1">
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-responsive-nav-link>
</div>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200">
<div class="px-4">
<div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
</div>
<div class="mt-3 space-y-1">
<x-responsive-nav-link :href="route('profile.edit')">
{{ __('Profile') }}
</x-responsive-nav-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-responsive-nav-link :href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-responsive-nav-link>
</form>
</div>
</div>
</div>
</nav>

View File

@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="ms">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>@yield('title', 'eCert MBIP')</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<style>
body { background: #f0f4f8; min-height: 100vh; }
.public-container { max-width: 480px; margin: 0 auto; }
</style>
@stack('styles')
</head>
<body>
{{-- Header Brand --}}
<div class="public-hero">
<div class="public-container px-3 pt-2 pb-3">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-award-fill fs-4 me-2 opacity-75"></i>
<span class="fw-bold" style="font-size:.9rem; letter-spacing:.5px;">eCert MBIP</span>
</div>
@yield('hero')
</div>
</div>
{{-- Main Content --}}
<div class="public-container px-3 py-4">
{{-- Flash Messages --}}
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show mb-4" role="alert">
<i class="bi bi-check-circle-fill me-2"></i>{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show mb-4" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>{{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('warning'))
<div class="alert alert-warning alert-dismissible fade show mb-4" role="alert">
<i class="bi bi-exclamation-circle-fill me-2"></i>{{ session('warning') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@yield('content')
</div>
{{-- Footer --}}
<div class="text-center py-4">
<small class="text-muted">Majlis Bandaraya Ipoh Perak &copy; {{ date('Y') }}</small>
</div>
@stack('scripts')
</body>
</html>