first commit
This commit is contained in:
18
.editorconfig
Normal file
18
.editorconfig
Normal file
@@ -0,0 +1,18 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[compose.yaml]
|
||||
indent_size = 4
|
||||
65
.env.example
Normal file
65
.env.example
Normal file
@@ -0,0 +1,65 @@
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
# APP_MAINTENANCE_STORE=database
|
||||
|
||||
# PHP_CLI_SERVER_WORKERS=4
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=laravel
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
# CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
11
.gitattributes
vendored
Normal file
11
.gitattributes
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
/.fleet
|
||||
/.idea
|
||||
/.nova
|
||||
/.phpunit.cache
|
||||
/.vscode
|
||||
/.zed
|
||||
/auth.json
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/vendor
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
Thumbs.db
|
||||
59
README.md
Normal file
59
README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||
</p>
|
||||
|
||||
## About Laravel
|
||||
|
||||
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||
|
||||
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||
|
||||
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||
|
||||
## Learning Laravel
|
||||
|
||||
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application.
|
||||
|
||||
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||
|
||||
## Laravel Sponsors
|
||||
|
||||
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
|
||||
|
||||
### Premium Partners
|
||||
|
||||
- **[Vehikl](https://vehikl.com)**
|
||||
- **[Tighten Co.](https://tighten.co)**
|
||||
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
||||
- **[64 Robots](https://64robots.com)**
|
||||
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
|
||||
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
|
||||
- **[Redberry](https://redberry.international/laravel-development)**
|
||||
- **[Active Logic](https://activelogic.com)**
|
||||
|
||||
## Contributing
|
||||
|
||||
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||
|
||||
## Security Vulnerabilities
|
||||
|
||||
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||
|
||||
## License
|
||||
|
||||
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||
53
app/Http/Controllers/Admin/DashboardController.php
Normal file
53
app/Http/Controllers/Admin/DashboardController.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Survey;
|
||||
use App\Models\Response;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
// count total borang
|
||||
$totalSurveys = Survey::count();
|
||||
|
||||
// Count total respons
|
||||
$totalResponses = Response::count();
|
||||
|
||||
// Count total users
|
||||
$totalUsers = User::count();
|
||||
|
||||
// 5 recent borang
|
||||
$recentSurveys = Survey::with(['user', 'responses'])
|
||||
->latest()
|
||||
->take(5)
|
||||
->get();
|
||||
|
||||
// Recent Reviews (Ulasan) - "sikit je" (e.g., 5)
|
||||
$recentReviews = Survey::whereNotNull('ulasan')
|
||||
->where('ulasan', '!=', '')
|
||||
->with('user') // Assuming we might want to know who created the survey
|
||||
->latest('updated_at')
|
||||
->take(5)
|
||||
->get();
|
||||
|
||||
// Recent Users - "sikit je" (e.g., 5)
|
||||
$recentUsers = User::latest()
|
||||
->take(5)
|
||||
->get();
|
||||
|
||||
return view('admin.dashboard', compact(
|
||||
'totalSurveys',
|
||||
'totalResponses',
|
||||
'totalUsers',
|
||||
'recentSurveys',
|
||||
'recentReviews',
|
||||
'recentUsers'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
49
app/Http/Controllers/Admin/ResponseController.php
Normal file
49
app/Http/Controllers/Admin/ResponseController.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Models\Survey;
|
||||
use App\Models\Response;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class ResponseController extends Controller
|
||||
{
|
||||
|
||||
public function list(Request $request)
|
||||
{
|
||||
$query = Survey::withCount('responses');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where('title', 'like', "%{$search}%");
|
||||
}
|
||||
|
||||
$surveys = $query->latest()->get();
|
||||
|
||||
return view('admin.responses.list', compact('surveys'));
|
||||
}
|
||||
|
||||
public function showRespondents(Survey $survey)
|
||||
{
|
||||
$responses = $survey->responses()
|
||||
->with('user')
|
||||
->orderByDesc('created_at')
|
||||
->get();
|
||||
|
||||
return view('admin.responses.respondents', compact('survey', 'responses'));
|
||||
}
|
||||
|
||||
|
||||
public function detail(Response $response)
|
||||
{
|
||||
$response->load([
|
||||
'survey.sections.questions',
|
||||
'answers.question'
|
||||
]);
|
||||
|
||||
$answersByQuestionId = $response->answers->keyBy('question_id');
|
||||
|
||||
return view('admin.responses.detail', compact('response', 'answersByQuestionId'));
|
||||
}
|
||||
}
|
||||
173
app/Http/Controllers/Admin/StatsController.php
Normal file
173
app/Http/Controllers/Admin/StatsController.php
Normal file
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Models\Survey;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
use App\Models\Question;
|
||||
use App\Models\Response;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class StatsController extends Controller
|
||||
{
|
||||
public function showStats($id)
|
||||
{
|
||||
$survey = Survey::with(['sections.questions.options', 'sections.questions.answers'])->findOrFail($id);
|
||||
|
||||
$totalSurveyRespondents = \App\Models\Response::where('survey_id', $id)->count();
|
||||
$totalSurveyQuestions = \App\Models\Question::whereHas('section', function ($q) use ($id) {
|
||||
$q->where('survey_id', $id);
|
||||
})->count();
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ($survey->sections as $section) {
|
||||
foreach ($section->questions as $question) {
|
||||
|
||||
$totalRespondents = $question->answers->count();
|
||||
$stats = [];
|
||||
$allAnswers = $question->answers->pluck('answer_text')->map(fn($item) => trim($item))->toArray();
|
||||
|
||||
// HANDLE TEXT QUESTIONS
|
||||
if ($question->type === 'text') {
|
||||
$results[$question->id] = [
|
||||
'question' => $question->question_text,
|
||||
'type' => 'text',
|
||||
'total' => $totalRespondents,
|
||||
'data' => array_filter($allAnswers) // Filter empty answers if any
|
||||
];
|
||||
continue; // Skip the rest of the loop (chart logic)
|
||||
}
|
||||
|
||||
// HANDLE RADIO/CHECKBOX (EXISTING LOGIC)
|
||||
foreach ($question->options as $option) {
|
||||
$optionLabel = trim($option->option_text);
|
||||
$count = 0;
|
||||
foreach ($allAnswers as $ans) {
|
||||
if ($ans === $optionLabel) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
$stats[] = [
|
||||
'label' => $optionLabel,
|
||||
'count' => $count,
|
||||
'percentage' => $totalRespondents > 0 ? round(($count / $totalRespondents) * 100, 1) : 0
|
||||
];
|
||||
}
|
||||
|
||||
$optionTexts = $question->options->pluck('option_text')->map(fn($item) => trim($item))->toArray();
|
||||
$othersCount = 0;
|
||||
$otherAnswersList = [];
|
||||
|
||||
foreach ($allAnswers as $ans) {
|
||||
if (!in_array($ans, $optionTexts)) {
|
||||
$othersCount++;
|
||||
// Clean up "Lain-lain: " prefix if exists (based on public view logic)
|
||||
$cleanAns = str_replace("Lain-lain: ", "", $ans);
|
||||
$otherAnswersList[] = $cleanAns;
|
||||
}
|
||||
}
|
||||
|
||||
if ($question->allow_other_option && $othersCount > 0) {
|
||||
$stats[] = [
|
||||
'label' => 'Lain-lain',
|
||||
'count' => $othersCount,
|
||||
'percentage' => $totalRespondents > 0 ? round(($othersCount / $totalRespondents) * 100, 1) : 0
|
||||
];
|
||||
}
|
||||
|
||||
$results[$question->id] = [
|
||||
'question' => $question->question_text,
|
||||
'type' => 'chart', // Mark as chart
|
||||
'total' => $totalRespondents,
|
||||
'data' => $stats,
|
||||
'other_answers' => $otherAnswersList
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return view('admin.surveys.statistics', compact('survey', 'results', 'totalSurveyRespondents', 'totalSurveyQuestions'));
|
||||
}
|
||||
|
||||
public function printStats($id)
|
||||
{
|
||||
$survey = Survey::with(['sections.questions.options', 'sections.questions.answers'])->findOrFail($id);
|
||||
|
||||
$totalSurveyRespondents = \App\Models\Response::where('survey_id', $id)->count();
|
||||
$totalSurveyQuestions = \App\Models\Question::whereHas('section', function ($q) use ($id) {
|
||||
$q->where('survey_id', $id);
|
||||
})->count();
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ($survey->sections as $section) {
|
||||
foreach ($section->questions as $question) {
|
||||
|
||||
$totalRespondents = $question->answers->count();
|
||||
$stats = [];
|
||||
$allAnswers = $question->answers->pluck('answer_text')->map(fn($item) => trim($item))->toArray();
|
||||
|
||||
// HANDLE TEXT QUESTIONS
|
||||
if ($question->type === 'text') {
|
||||
$results[$question->id] = [
|
||||
'question' => $question->question_text,
|
||||
'type' => 'text',
|
||||
'total' => $totalRespondents,
|
||||
'data' => array_filter($allAnswers) // Filter empty answers if any
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
// HANDLE RADIO/CHECKBOX (EXISTING LOGIC)
|
||||
foreach ($question->options as $option) {
|
||||
$optionLabel = trim($option->option_text);
|
||||
$count = 0;
|
||||
foreach ($allAnswers as $ans) {
|
||||
if ($ans === $optionLabel) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
$stats[] = [
|
||||
'label' => $optionLabel,
|
||||
'count' => $count,
|
||||
'percentage' => $totalRespondents > 0 ? round(($count / $totalRespondents) * 100, 1) : 0
|
||||
];
|
||||
}
|
||||
|
||||
$optionTexts = $question->options->pluck('option_text')->map(fn($item) => trim($item))->toArray();
|
||||
$othersCount = 0;
|
||||
$otherAnswersList = [];
|
||||
|
||||
foreach ($allAnswers as $ans) {
|
||||
if (!in_array($ans, $optionTexts)) {
|
||||
$othersCount++;
|
||||
// Clean up "Lain-lain: " prefix if exists
|
||||
$cleanAns = str_replace("Lain-lain: ", "", $ans);
|
||||
$otherAnswersList[] = $cleanAns;
|
||||
}
|
||||
}
|
||||
|
||||
if ($question->allow_other_option && $othersCount > 0) {
|
||||
$stats[] = [
|
||||
'label' => 'Lain-lain',
|
||||
'count' => $othersCount,
|
||||
'percentage' => $totalRespondents > 0 ? round(($othersCount / $totalRespondents) * 100, 1) : 0
|
||||
];
|
||||
}
|
||||
|
||||
$results[$question->id] = [
|
||||
'question' => $question->question_text,
|
||||
'type' => 'chart',
|
||||
'total' => $totalRespondents,
|
||||
'data' => $stats,
|
||||
'other_answers' => $otherAnswersList
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return view('admin.surveys.print_statistics', compact('survey', 'results', 'totalSurveyRespondents', 'totalSurveyQuestions'));
|
||||
}
|
||||
}
|
||||
209
app/Http/Controllers/Admin/SurveyController.php
Normal file
209
app/Http/Controllers/Admin/SurveyController.php
Normal file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Models\Survey;
|
||||
use App\Models\Section;
|
||||
use App\Models\Question;
|
||||
use App\Models\Option;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class SurveyController extends Controller
|
||||
{
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = Survey::with('user', 'questions', 'responses');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$s = $request->search;
|
||||
$query->where('title', 'like', "%$s%");
|
||||
}
|
||||
|
||||
$surveys = $query->latest()->paginate(20);
|
||||
|
||||
return view('admin.surveys.index', compact('surveys'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('admin.surveys.create');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'perincian' => 'nullable|string',
|
||||
'date' => 'required|date',
|
||||
'sections' => 'required|array|min:1',
|
||||
'sections.*.title' => 'required|string',
|
||||
'sections.*.questions' => 'required|array|min:1',
|
||||
'sections.*.questions.*.text' => 'required|string',
|
||||
'sections.*.questions.*.type' => 'required|in:radio,text,checkbox',
|
||||
'sections.*.questions.*.allow_other_option' => 'nullable|boolean',
|
||||
'sections.*.questions.*.options' => 'required_if:sections.*.questions.*.type,radio|required_if:sections.*.questions.*.type,checkbox|array',
|
||||
'sections.*.questions.*.options.*' => 'required|string',
|
||||
]);
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$survey = Survey::create([
|
||||
'title' => $request->title,
|
||||
'perincian' => $request->perincian,
|
||||
'date' => $request->date,
|
||||
'user_id' => Auth::id(),
|
||||
]);
|
||||
|
||||
|
||||
foreach ($request->sections as $sectionIndex => $sectionData) {
|
||||
$section = $survey->sections()->create([
|
||||
'title' => $sectionData['title'],
|
||||
'description' => $sectionData['description'] ?? null,
|
||||
'order' => $sectionIndex,
|
||||
]);
|
||||
|
||||
|
||||
foreach ($sectionData['questions'] as $questionIndex => $questionData) {
|
||||
$question = $section->questions()->create([
|
||||
'question_text' => $questionData['text'],
|
||||
'type' => $questionData['type'],
|
||||
'allow_other_option' => $questionData['allow_other_option'] ?? false,
|
||||
'order' => $questionIndex,
|
||||
]);
|
||||
|
||||
|
||||
if (isset($questionData['options'])) {
|
||||
foreach ($questionData['options'] as $optionText) {
|
||||
$question->options()->create([
|
||||
'option_text' => $optionText,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
\Log::error($e->getMessage());
|
||||
return redirect()->back()->with('error', 'Gagal menyimpan borang. Sila cuba lagi.')->withInput();
|
||||
}
|
||||
|
||||
return redirect()->route('admin.surveys.index')->with('success', 'Borang berjaya dicipta.');
|
||||
}
|
||||
|
||||
public function edit(Survey $survey)
|
||||
{
|
||||
$survey->load('sections.questions.options');
|
||||
|
||||
$sections_with_questions = $survey->sections->map(function ($section) {
|
||||
return [
|
||||
'title' => $section->title,
|
||||
'description' => $section->description,
|
||||
'questions' => $section->questions->map(function ($q) {
|
||||
return [
|
||||
'text' => $q->question_text,
|
||||
'type' => $q->type,
|
||||
'allow_other_option' => (bool) $q->allow_other_option,
|
||||
'options' => $q->options->map(function ($opt) {
|
||||
return ['text' => $opt->option_text];
|
||||
})->toArray(),
|
||||
];
|
||||
})->toArray(),
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
return view('admin.surveys.edit', compact('survey', 'sections_with_questions'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Survey $survey)
|
||||
{
|
||||
$request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'perincian' => 'nullable|string',
|
||||
'date' => 'required|date',
|
||||
'sections' => 'required|array|min:1',
|
||||
'sections.*.title' => 'required|string',
|
||||
'sections.*.questions' => 'required|array|min:1',
|
||||
'sections.*.questions.*.text' => 'required|string',
|
||||
'sections.*.questions.*.type' => 'required|in:radio,text,checkbox',
|
||||
'sections.*.questions.*.allow_other_option' => 'nullable|boolean',
|
||||
'sections.*.questions.*.options' => 'required_if:sections.*.questions.*.type,radio|required_if:sections.*.questions.*.type,checkbox|array',
|
||||
'sections.*.questions.*.options.*' => 'required|string',
|
||||
]);
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$survey->update([
|
||||
'title' => $request->title,
|
||||
'perincian' => $request->perincian,
|
||||
'date' => $request->date,
|
||||
|
||||
]);
|
||||
|
||||
foreach ($survey->sections as $oldSection) {
|
||||
foreach ($oldSection->questions as $oldQuestion) {
|
||||
|
||||
$oldQuestion->options()->delete();
|
||||
}
|
||||
|
||||
$oldSection->questions()->delete();
|
||||
}
|
||||
|
||||
$survey->sections()->delete();
|
||||
|
||||
foreach ($request->sections as $sectionIndex => $sectionData) {
|
||||
$section = $survey->sections()->create([
|
||||
'title' => $sectionData['title'],
|
||||
'description' => $sectionData['description'] ?? null,
|
||||
'order' => $sectionIndex,
|
||||
]);
|
||||
|
||||
foreach ($sectionData['questions'] as $questionIndex => $questionData) {
|
||||
$question = $section->questions()->create([
|
||||
'question_text' => $questionData['text'],
|
||||
'type' => $questionData['type'],
|
||||
'allow_other_option' => $questionData['allow_other_option'] ?? false,
|
||||
'order' => $questionIndex,
|
||||
]);
|
||||
|
||||
if (isset($questionData['options']) && is_array($questionData['options'])) {
|
||||
foreach ($questionData['options'] as $optionText) {
|
||||
if (is_array($optionText)) {
|
||||
$optionText = $optionText['text'] ?? '';
|
||||
}
|
||||
$question->options()->create([
|
||||
'option_text' => $optionText,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
\Log::error('Survey update failed: ' . $e->getMessage());
|
||||
return redirect()->back()->with('error', 'Gagal kemaskini borang. Sila cuba lagi.')->withInput();
|
||||
}
|
||||
|
||||
return redirect()->route('admin.surveys.index')->with('success', 'Borang berjaya dikemaskini.');
|
||||
}
|
||||
|
||||
public function destroy(Survey $survey)
|
||||
{
|
||||
$survey->delete();
|
||||
return redirect()->route('admin.surveys.index')->with('success', 'Borang berjaya dihapuskan.');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
70
app/Http/Controllers/Admin/UlasanController.php
Normal file
70
app/Http/Controllers/Admin/UlasanController.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
|
||||
class UlasanController extends Controller
|
||||
{
|
||||
public function ulasanPage(Request $request)
|
||||
{
|
||||
$query = \App\Models\Survey::whereNotNull('ulasan')
|
||||
->where('ulasan', '!=', '');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where('title', 'like', "%{$search}%");
|
||||
}
|
||||
|
||||
$surveys = $query->orderBy('updated_at', 'desc')->get();
|
||||
|
||||
return view('admin.surveys.ulasan', compact('surveys'));
|
||||
}
|
||||
|
||||
public function updateUlasan(Request $request, $id)
|
||||
{
|
||||
$survey = \App\Models\Survey::findOrFail($id);
|
||||
|
||||
$survey->ulasan = $request->ulasan;
|
||||
$survey->save();
|
||||
|
||||
return back()->with('success', 'Ulasan berjaya dikemaskini!');
|
||||
}
|
||||
|
||||
public function downloadCSV()
|
||||
{
|
||||
$surveys = \App\Models\Survey::whereNotNull('ulasan')
|
||||
->where('ulasan', '!=', '')
|
||||
->orderBy('updated_at', 'desc')
|
||||
->get();
|
||||
|
||||
$filename = "keputusan_postmortem" . date('Ymd_His') . ".csv";
|
||||
$handle = fopen('php://output', 'w');
|
||||
|
||||
// Add UTF-8 BOM for Excel compatibility
|
||||
fprintf($handle, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||||
|
||||
// Header
|
||||
fputcsv($handle, ['ID', 'Tarikh Kemaskini', 'Tajuk Borang', 'Ulasan']);
|
||||
|
||||
// Data
|
||||
foreach ($surveys as $survey) {
|
||||
fputcsv($handle, [
|
||||
$survey->id,
|
||||
$survey->updated_at->format('d/m/Y'),
|
||||
$survey->title,
|
||||
$survey->ulasan
|
||||
]);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
return response()->streamDownload(function () use ($handle) {
|
||||
// Already handled by fputcsv to php://output
|
||||
}, $filename, [
|
||||
'Content-Type' => 'text/csv',
|
||||
]);
|
||||
}
|
||||
}
|
||||
89
app/Http/Controllers/AdminController.php
Normal file
89
app/Http/Controllers/AdminController.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class AdminController extends Controller
|
||||
{
|
||||
public function dashboard()
|
||||
{
|
||||
return view('admin.dashboard');
|
||||
}
|
||||
|
||||
public function users(Request $request)
|
||||
{
|
||||
$query = User::query();
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$s = $request->search;
|
||||
$query->where(function ($q) use ($s) {
|
||||
$q->where('name', 'like', "%$s%")
|
||||
->orWhere('no_pekerja', 'like', "%$s%")
|
||||
->orWhere('jabatan', 'like', "%$s%");
|
||||
});
|
||||
}
|
||||
|
||||
$users = $query->get();
|
||||
return view('admin.users.index', compact('users'));
|
||||
}
|
||||
|
||||
public function storeUser(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name'=>'required|string|max:255',
|
||||
'password'=>'required|string|min:6',
|
||||
'role'=>'required|in:admin,staff',
|
||||
'no_pekerja'=>'required|string|unique:users,no_pekerja',
|
||||
'jabatan'=>'required|string|max:255',
|
||||
]);
|
||||
|
||||
User::create([
|
||||
'name'=>$request->name,
|
||||
'password'=>Hash::make($request->password),
|
||||
'role'=>$request->role,
|
||||
'no_pekerja'=>$request->no_pekerja,
|
||||
'jabatan' => $request->jabatan,
|
||||
]);
|
||||
|
||||
return redirect()->back()->with('success','Pengguna telah ditambah');
|
||||
}
|
||||
|
||||
public function updateUser(Request $request, $id)
|
||||
{
|
||||
$user = User::findOrFail($id);
|
||||
|
||||
$request->validate([
|
||||
'name'=>'required|string|max:255',
|
||||
'role'=>'required|in:admin,staff',
|
||||
'no_pekerja'=>'required|unique:users,no_pekerja,'.$user->id,
|
||||
'jabatan'=>'required|string|max:255',
|
||||
]);
|
||||
|
||||
$user->update([
|
||||
'name'=>$request->name,
|
||||
'role'=>$request->role,
|
||||
'no_pekerja'=>$request->no_pekerja,
|
||||
'jabatan' => $request->jabatan,
|
||||
]);
|
||||
|
||||
if($request->password){
|
||||
$request->validate(['password' => 'string|min:3']);
|
||||
$user->update(['password'=>Hash::make($request->password)]);
|
||||
}
|
||||
|
||||
return redirect()->back()->with('success','Pengguna telah dikemaskini');
|
||||
}
|
||||
|
||||
public function deleteUser($id)
|
||||
{
|
||||
$user = User::findOrFail($id);
|
||||
$user->delete();
|
||||
|
||||
return redirect()->back()->with('success','Pengguna telah dihapuskan');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
47
app/Http/Controllers/AuthController.php
Normal file
47
app/Http/Controllers/AuthController.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
public function showLogin()
|
||||
{
|
||||
return view('auth.login');
|
||||
}
|
||||
|
||||
public function doLogin(Request $request)
|
||||
{
|
||||
$credentials = $request->validate([
|
||||
'no_pekerja' => ['required',],
|
||||
'password' => ['required']
|
||||
]);
|
||||
|
||||
if (Auth::attempt($credentials)) {
|
||||
$request->session()->regenerate();
|
||||
|
||||
$role = auth()->user()->role;
|
||||
if ($role == 'admin') {
|
||||
return redirect()->route('admin.dashboard');
|
||||
} else {
|
||||
Auth::logout();
|
||||
return redirect()->route('login')->withErrors(['no_pekerja' => 'Akses dihalang. Admin sahaja.']);
|
||||
}
|
||||
}
|
||||
|
||||
return back()->withErrors([
|
||||
'no_pekerja' => 'Kata laluan atau nombor pekerja tidak betul.'
|
||||
]);
|
||||
}
|
||||
|
||||
public function logout(Request $request)
|
||||
{
|
||||
Auth::logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
return redirect()->route('login');
|
||||
}
|
||||
}
|
||||
8
app/Http/Controllers/Controller.php
Normal file
8
app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
71
app/Http/Controllers/StaffSurveyController.php
Normal file
71
app/Http/Controllers/StaffSurveyController.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Survey;
|
||||
use App\Models\Response;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class StaffSurveyController extends Controller
|
||||
{
|
||||
public function show(Survey $survey)
|
||||
{
|
||||
$survey->load('sections.questions.options');
|
||||
return view('staff.surveys.soalan', compact('survey'));
|
||||
}
|
||||
|
||||
public function store(Request $request, Survey $survey)
|
||||
{
|
||||
$request->validate([
|
||||
'respondent_name' => 'required|string|max:255',
|
||||
'respondent_no_pekerja' => 'required|string|max:255',
|
||||
'respondent_jabatan' => 'required|string|max:255',
|
||||
'answers' => 'required|array',
|
||||
'answers.*' => 'required',
|
||||
], [
|
||||
'answers.*.required' => 'Sila jawab semua soalan yang bertanda *.'
|
||||
]);
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// 1. Cipta rekod Response utama
|
||||
$response = $survey->responses()->create([
|
||||
'user_id' => null, // Always null for public
|
||||
'respondent_name' => $request->respondent_name,
|
||||
'respondent_no_pekerja' => $request->respondent_no_pekerja,
|
||||
'respondent_jabatan' => $request->respondent_jabatan,
|
||||
]);
|
||||
|
||||
// 2. Simpan setiap jawapan soalan
|
||||
foreach ($request->answers as $question_id => $answer_data) {
|
||||
if (is_array($answer_data)) {
|
||||
// Untuk soalan jenis Checkbox atau Multiple Select
|
||||
foreach ($answer_data as $single_answer) {
|
||||
$response->answers()->create([
|
||||
'question_id' => $question_id,
|
||||
'answer_text' => $single_answer,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
// Untuk soalan jenis Radio, Text, atau Textarea
|
||||
$response->answers()->create([
|
||||
'question_id' => $question_id,
|
||||
'answer_text' => $answer_data,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
return redirect()->route('surveys.success');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
// Log ralat jika perlu: \Log::error($e->getMessage());
|
||||
return redirect()->back()->with('error', 'Gagal menghantar jawapan. Sila cuba lagi.');
|
||||
}
|
||||
}
|
||||
}
|
||||
33
app/Http/Middleware/RoleMiddleware.php
Normal file
33
app/Http/Middleware/RoleMiddleware.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Auth;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class RoleMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, $role): Response
|
||||
{
|
||||
if ($request->routeIs('staff.surveys.soalan')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if (!auth::check()) {
|
||||
return redirect()->route('login');
|
||||
}
|
||||
|
||||
if (Auth::user()->role != $role) {
|
||||
abort(403, 'Tak boleh la begitu ji.');
|
||||
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
26
app/Models/Answer.php
Normal file
26
app/Models/Answer.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Answer extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'response_id',
|
||||
'question_id',
|
||||
'option_id',
|
||||
'answer_text',
|
||||
];
|
||||
|
||||
public function response()
|
||||
{
|
||||
return $this->belongsTo(Response::class);
|
||||
}
|
||||
|
||||
public function question()
|
||||
{
|
||||
return $this->belongsTo(Question::class);
|
||||
}
|
||||
|
||||
}
|
||||
14
app/Models/Option.php
Normal file
14
app/Models/Option.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Option extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'question_id',
|
||||
'option_text',
|
||||
];
|
||||
|
||||
}
|
||||
33
app/Models/Question.php
Normal file
33
app/Models/Question.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Question extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'section_id',
|
||||
'question_text',
|
||||
'type',
|
||||
'allow_other_option',
|
||||
'order',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'lain_lain' => 'boolean',
|
||||
];
|
||||
|
||||
public function section()
|
||||
{
|
||||
return $this->belongsTo(Section::class);
|
||||
}
|
||||
public function options()
|
||||
{
|
||||
return $this->hasMany(Option::class);
|
||||
}
|
||||
public function answers()
|
||||
{
|
||||
return $this->hasMany(Answer::class);
|
||||
}
|
||||
}
|
||||
30
app/Models/Response.php
Normal file
30
app/Models/Response.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Response extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'survey_id',
|
||||
'user_id',
|
||||
'respondent_name',
|
||||
'respondent_no_pekerja',
|
||||
'respondent_jabatan',
|
||||
];
|
||||
|
||||
|
||||
public function survey()
|
||||
{
|
||||
return $this->belongsTo(Survey::class);
|
||||
}
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
} // Responden
|
||||
public function answers()
|
||||
{
|
||||
return $this->hasMany(Answer::class);
|
||||
}
|
||||
}
|
||||
18
app/Models/Section.php
Normal file
18
app/Models/Section.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Section extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'survey_id',
|
||||
'title',
|
||||
'description',
|
||||
'order',
|
||||
];
|
||||
|
||||
public function survey() { return $this->belongsTo(Survey::class); }
|
||||
public function questions() { return $this->hasMany(Question::class); }
|
||||
}
|
||||
32
app/Models/Survey.php
Normal file
32
app/Models/Survey.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Survey extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'date',
|
||||
'user_id',
|
||||
'uuid',
|
||||
'perincian',
|
||||
];
|
||||
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($model) {
|
||||
if (empty($model->uuid)) {
|
||||
$model->uuid = (string) \Illuminate\Support\Str::uuid();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function user() { return $this->belongsTo(User::class); }
|
||||
public function sections() { return $this->hasMany(Section::class); }
|
||||
public function questions() { return $this->hasManyThrough(Question::class, Section::class); }
|
||||
public function responses() { return $this->hasMany(Response::class); }
|
||||
}
|
||||
71
app/Models/User.php
Normal file
71
app/Models/User.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'password',
|
||||
'role',
|
||||
'no_pekerja',
|
||||
'jabatan',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
// Helper: isAdmin
|
||||
public function isAdmin(): bool {
|
||||
return $this->role === 'admin';
|
||||
}
|
||||
|
||||
// Helper: isStaff
|
||||
public function isStaff(): bool {
|
||||
return $this->role === 'staff';
|
||||
}
|
||||
|
||||
public function scopeSearch($query, $keyword)
|
||||
{
|
||||
return $query->where('name', 'like', "%$keyword%")
|
||||
->orWhere('no_pekerja', 'like', "%$keyword%")
|
||||
->orWhere('jabatan', 'like', "%$keyword%");
|
||||
}
|
||||
|
||||
public function surveys() { return $this->hasMany(Survey::class); }
|
||||
public function responses() { return $this->hasMany(Response::class); }
|
||||
|
||||
}
|
||||
24
app/Providers/AppServiceProvider.php
Normal file
24
app/Providers/AppServiceProvider.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
\Illuminate\Pagination\Paginator::useBootstrapFive();
|
||||
}
|
||||
}
|
||||
18
artisan
Normal file
18
artisan
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the command...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
$status = $app->handleCommand(new ArgvInput);
|
||||
|
||||
exit($status);
|
||||
20
bootstrap/app.php
Normal file
20
bootstrap/app.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__ . '/../routes/web.php',
|
||||
commands: __DIR__ . '/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
$middleware->alias([
|
||||
'role' => \App\Http\Middleware\RoleMiddleware::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
})->create();
|
||||
2
bootstrap/cache/.gitignore
vendored
Normal file
2
bootstrap/cache/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
5
bootstrap/providers.php
Normal file
5
bootstrap/providers.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
];
|
||||
86
composer.json
Normal file
86
composer.json
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"$schema": "https://getcomposer.org/schema.json",
|
||||
"name": "laravel/laravel",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/tinker": "^2.10.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/pint": "^1.24",
|
||||
"laravel/sail": "^1.41",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"phpunit/phpunit": "^11.5.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"setup": [
|
||||
"composer install",
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||
"@php artisan key:generate",
|
||||
"@php artisan migrate --force",
|
||||
"npm install",
|
||||
"npm run build"
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||
],
|
||||
"test": [
|
||||
"@php artisan config:clear --ansi",
|
||||
"@php artisan test"
|
||||
],
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi",
|
||||
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||
"@php artisan migrate --graceful --ansi"
|
||||
],
|
||||
"pre-package-uninstall": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
8358
composer.lock
generated
Normal file
8358
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
126
config/app.php
Normal file
126
config/app.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application, which will be used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| other UI elements where an application name needs to be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Laravel'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Environment
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the "environment" your application is currently
|
||||
| running in. This may determine how you prefer to configure various
|
||||
| services the application utilizes. Set this in your ".env" file.
|
||||
|
|
||||
*/
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When your application is in debug mode, detailed error messages with
|
||||
| stack traces will be shown on every error that occurs within your
|
||||
| application. If disabled, a simple generic error page is shown.
|
||||
|
|
||||
*/
|
||||
|
||||
'debug' => (bool) env('APP_DEBUG', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This URL is used by the console to properly generate URLs when using
|
||||
| the Artisan command line tool. You should set this to the root of
|
||||
| the application so that it's available within Artisan commands.
|
||||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default timezone for your application, which
|
||||
| will be used by the PHP date and date-time functions. The timezone
|
||||
| is set to "UTC" by default as it is suitable for most use cases.
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Locale Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The application locale determines the default locale that will be used
|
||||
| by Laravel's translation / localization methods. This option can be
|
||||
| set to any locale for which you plan to have translation strings.
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
|
||||
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This key is utilized by Laravel's encryption services and should be set
|
||||
| to a random, 32 character string to ensure that all encrypted values
|
||||
| are secure. You should do this prior to deploying the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||
),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maintenance Mode Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options determine the driver used to determine and
|
||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||
| allow maintenance mode to be controlled across multiple machines.
|
||||
|
|
||||
| Supported drivers: "file", "cache"
|
||||
|
|
||||
*/
|
||||
|
||||
'maintenance' => [
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
];
|
||||
115
config/auth.php
Normal file
115
config/auth.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default authentication "guard" and password
|
||||
| reset "broker" for your application. You may change these values
|
||||
| as required, but they're a perfect start for most applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
'guard' => env('AUTH_GUARD', 'web'),
|
||||
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Next, you may define every authentication guard for your application.
|
||||
| Of course, a great default configuration has been defined for you
|
||||
| which utilizes session storage plus the Eloquent user provider.
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| Supported: "session"
|
||||
|
|
||||
*/
|
||||
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| If you have multiple user tables or models you may configure multiple
|
||||
| providers to represent the model / table. These providers may then
|
||||
| be assigned to any extra authentication guards you have defined.
|
||||
|
|
||||
| Supported: "database", "eloquent"
|
||||
|
|
||||
*/
|
||||
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
// 'driver' => 'database',
|
||||
// 'table' => 'users',
|
||||
// ],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Resetting Passwords
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options specify the behavior of Laravel's password
|
||||
| reset functionality, including the table utilized for token storage
|
||||
| and the user provider that is invoked to actually retrieve users.
|
||||
|
|
||||
| The expiry time is the number of minutes that each reset token will be
|
||||
| considered valid. This security feature keeps tokens short-lived so
|
||||
| they have less time to be guessed. You may change this as needed.
|
||||
|
|
||||
| The throttle setting is the number of seconds a user must wait before
|
||||
| generating more password reset tokens. This prevents the user from
|
||||
| quickly generating a very large amount of password reset tokens.
|
||||
|
|
||||
*/
|
||||
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Confirmation Timeout
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the number of seconds before a password confirmation
|
||||
| window expires and users are asked to re-enter their password via the
|
||||
| confirmation screen. By default, the timeout lasts for three hours.
|
||||
|
|
||||
*/
|
||||
|
||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||
|
||||
];
|
||||
117
config/cache.php
Normal file
117
config/cache.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default cache store that will be used by the
|
||||
| framework. This connection is utilized if another isn't explicitly
|
||||
| specified when running a cache operation inside the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('CACHE_STORE', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Stores
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define all of the cache "stores" for your application as
|
||||
| well as their drivers. You may even define multiple stores for the
|
||||
| same cache driver to group types of items stored in your caches.
|
||||
|
|
||||
| Supported drivers: "array", "database", "file", "memcached",
|
||||
| "redis", "dynamodb", "octane",
|
||||
| "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'stores' => [
|
||||
|
||||
'array' => [
|
||||
'driver' => 'array',
|
||||
'serialize' => false,
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_CACHE_CONNECTION'),
|
||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||
],
|
||||
|
||||
'file' => [
|
||||
'driver' => 'file',
|
||||
'path' => storage_path('framework/cache/data'),
|
||||
'lock_path' => storage_path('framework/cache/data'),
|
||||
],
|
||||
|
||||
'memcached' => [
|
||||
'driver' => 'memcached',
|
||||
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||
'sasl' => [
|
||||
env('MEMCACHED_USERNAME'),
|
||||
env('MEMCACHED_PASSWORD'),
|
||||
],
|
||||
'options' => [
|
||||
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||
],
|
||||
'servers' => [
|
||||
[
|
||||
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||
'port' => env('MEMCACHED_PORT', 11211),
|
||||
'weight' => 100,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||
],
|
||||
|
||||
'dynamodb' => [
|
||||
'driver' => 'dynamodb',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||
],
|
||||
|
||||
'octane' => [
|
||||
'driver' => 'octane',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'stores' => [
|
||||
'database',
|
||||
'array',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Key Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||
| stores, there might be other applications using the same cache. For
|
||||
| that reason, you may prefix every cache key to avoid collisions.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||
|
||||
];
|
||||
183
config/database.php
Normal file
183
config/database.php
Normal file
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Database Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which of the database connections below you wish
|
||||
| to use as your default connection for database operations. This is
|
||||
| the connection which will be utilized unless another connection
|
||||
| is explicitly specified when you execute a query / statement.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below are all of the database connections defined for your application.
|
||||
| An example configuration is provided for each database system which
|
||||
| is supported by Laravel. You're free to add / remove connections.
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DB_URL'),
|
||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
'busy_timeout' => null,
|
||||
'journal_mode' => null,
|
||||
'synchronous' => null,
|
||||
'transaction_mode' => 'DEFERRED',
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'mariadb' => [
|
||||
'driver' => 'mariadb',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'pgsql' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => 'prefer',
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
'driver' => 'sqlsrv',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', 'localhost'),
|
||||
'port' => env('DB_PORT', '1433'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Migration Repository Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This table keeps track of all the migrations that have already run for
|
||||
| your application. Using this information, we can determine which of
|
||||
| the migrations on disk haven't actually been run on the database.
|
||||
|
|
||||
*/
|
||||
|
||||
'migrations' => [
|
||||
'table' => 'migrations',
|
||||
'update_date_on_publish' => true,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Redis Databases
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Redis is an open source, fast, and advanced key-value store that also
|
||||
| provides a richer body of commands than a typical key-value system
|
||||
| such as Memcached. You may define your connection settings here.
|
||||
|
|
||||
*/
|
||||
|
||||
'redis' => [
|
||||
|
||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||
|
||||
'options' => [
|
||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||
'persistent' => env('REDIS_PERSISTENT', false),
|
||||
],
|
||||
|
||||
'default' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_CACHE_DB', '1'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
80
config/filesystems.php
Normal file
80
config/filesystems.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Filesystem Disk
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default filesystem disk that should be used
|
||||
| by the framework. The "local" disk, as well as a variety of cloud
|
||||
| based disks are available to your application for file storage.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Filesystem Disks
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below you may configure as many filesystem disks as necessary, and you
|
||||
| may even configure multiple disks for the same driver. Examples for
|
||||
| most supported storage drivers are configured here for reference.
|
||||
|
|
||||
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||
|
|
||||
*/
|
||||
|
||||
'disks' => [
|
||||
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/private'),
|
||||
'serve' => true,
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => env('APP_URL').'/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION'),
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'url' => env('AWS_URL'),
|
||||
'endpoint' => env('AWS_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Symbolic Links
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the symbolic links that will be created when the
|
||||
| `storage:link` Artisan command is executed. The array keys should be
|
||||
| the locations of the links and the values should be their targets.
|
||||
|
|
||||
*/
|
||||
|
||||
'links' => [
|
||||
public_path('storage') => storage_path('app/public'),
|
||||
],
|
||||
|
||||
];
|
||||
132
config/logging.php
Normal file
132
config/logging.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\SyslogUdpHandler;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default log channel that is utilized to write
|
||||
| messages to your logs. The value provided here should match one of
|
||||
| the channels present in the list of "channels" configured below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('LOG_CHANNEL', 'stack'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Deprecations Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the log channel that should be used to log warnings
|
||||
| regarding deprecated PHP and library features. This allows you to get
|
||||
| your application ready for upcoming major versions of dependencies.
|
||||
|
|
||||
*/
|
||||
|
||||
'deprecations' => [
|
||||
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Log Channels
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the log channels for your application. Laravel
|
||||
| utilizes the Monolog PHP logging library, which includes a variety
|
||||
| of powerful log handlers and formatters that you're free to use.
|
||||
|
|
||||
| Available drivers: "single", "daily", "slack", "syslog",
|
||||
| "errorlog", "monolog", "custom", "stack"
|
||||
|
|
||||
*/
|
||||
|
||||
'channels' => [
|
||||
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
'single' => [
|
||||
'driver' => 'single',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'days' => env('LOG_DAILY_DAYS', 14),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'driver' => 'slack',
|
||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
|
||||
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||
'level' => env('LOG_LEVEL', 'critical'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'papertrail' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||
'handler_with' => [
|
||||
'host' => env('PAPERTRAIL_URL'),
|
||||
'port' => env('PAPERTRAIL_PORT'),
|
||||
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||
],
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'stderr' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => StreamHandler::class,
|
||||
'handler_with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'syslog' => [
|
||||
'driver' => 'syslog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'errorlog' => [
|
||||
'driver' => 'errorlog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'monolog',
|
||||
'handler' => NullHandler::class,
|
||||
],
|
||||
|
||||
'emergency' => [
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
118
config/mail.php
Normal file
118
config/mail.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Mailer
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default mailer that is used to send all email
|
||||
| messages unless another mailer is explicitly specified when sending
|
||||
| the message. All additional mailers can be configured within the
|
||||
| "mailers" array. Examples of each type of mailer are provided.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('MAIL_MAILER', 'log'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mailer Configurations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure all of the mailers used by your application plus
|
||||
| their respective settings. Several examples have been configured for
|
||||
| you and you are free to add your own as your application requires.
|
||||
|
|
||||
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||
| when delivering an email. You may specify which one you're using for
|
||||
| your mailers below. You may also add additional mailers if needed.
|
||||
|
|
||||
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||
| "postmark", "resend", "log", "array",
|
||||
| "failover", "roundrobin"
|
||||
|
|
||||
*/
|
||||
|
||||
'mailers' => [
|
||||
|
||||
'smtp' => [
|
||||
'transport' => 'smtp',
|
||||
'scheme' => env('MAIL_SCHEME'),
|
||||
'url' => env('MAIL_URL'),
|
||||
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||
'port' => env('MAIL_PORT', 2525),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'timeout' => null,
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'transport' => 'ses',
|
||||
],
|
||||
|
||||
'postmark' => [
|
||||
'transport' => 'postmark',
|
||||
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||
// 'client' => [
|
||||
// 'timeout' => 5,
|
||||
// ],
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'transport' => 'resend',
|
||||
],
|
||||
|
||||
'sendmail' => [
|
||||
'transport' => 'sendmail',
|
||||
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||
],
|
||||
|
||||
'log' => [
|
||||
'transport' => 'log',
|
||||
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||
],
|
||||
|
||||
'array' => [
|
||||
'transport' => 'array',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'transport' => 'failover',
|
||||
'mailers' => [
|
||||
'smtp',
|
||||
'log',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
'roundrobin' => [
|
||||
'transport' => 'roundrobin',
|
||||
'mailers' => [
|
||||
'ses',
|
||||
'postmark',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Global "From" Address
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You may wish for all emails sent by your application to be sent from
|
||||
| the same address. Here you may specify a name and address that is
|
||||
| used globally for all emails that are sent by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'from' => [
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||
],
|
||||
|
||||
];
|
||||
129
config/queue.php
Normal file
129
config/queue.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Queue Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Laravel's queue supports a variety of backends via a single, unified
|
||||
| API, giving you convenient access to each backend using identical
|
||||
| syntax for each. The default queue connection is defined below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the connection options for every queue backend
|
||||
| used by your application. An example configuration is provided for
|
||||
| each backend supported by Laravel. You're also free to add more.
|
||||
|
|
||||
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
|
||||
| "deferred", "background", "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||
'queue' => env('DB_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'beanstalkd' => [
|
||||
'driver' => 'beanstalkd',
|
||||
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => 0,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'sqs' => [
|
||||
'driver' => 'sqs',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||
'queue' => env('SQS_QUEUE', 'default'),
|
||||
'suffix' => env('SQS_SUFFIX'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||
'queue' => env('REDIS_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => null,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'deferred' => [
|
||||
'driver' => 'deferred',
|
||||
],
|
||||
|
||||
'background' => [
|
||||
'driver' => 'background',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'connections' => [
|
||||
'database',
|
||||
'deferred',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Job Batching
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following options configure the database and table that store job
|
||||
| batching information. These options can be updated to any database
|
||||
| connection and table which has been defined by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'batching' => [
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'job_batches',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Failed Queue Jobs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options configure the behavior of failed queue job logging so you
|
||||
| can control how and where failed jobs are stored. Laravel ships with
|
||||
| support for storing failed jobs in a simple file or in a database.
|
||||
|
|
||||
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => [
|
||||
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'failed_jobs',
|
||||
],
|
||||
|
||||
];
|
||||
38
config/services.php
Normal file
38
config/services.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Third Party Services
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This file is for storing the credentials for third party services such
|
||||
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||
| location for this type of information, allowing packages to have
|
||||
| a conventional file to locate the various service credentials.
|
||||
|
|
||||
*/
|
||||
|
||||
'postmark' => [
|
||||
'key' => env('POSTMARK_API_KEY'),
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'key' => env('RESEND_API_KEY'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'notifications' => [
|
||||
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
217
config/session.php
Normal file
217
config/session.php
Normal file
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Session Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines the default session driver that is utilized for
|
||||
| incoming requests. Laravel supports a variety of storage options to
|
||||
| persist session data. Database storage is a great default choice.
|
||||
|
|
||||
| Supported: "file", "cookie", "database", "memcached",
|
||||
| "redis", "dynamodb", "array"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SESSION_DRIVER', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Lifetime
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the number of minutes that you wish the session
|
||||
| to be allowed to remain idle before it expires. If you want them
|
||||
| to expire immediately when the browser is closed then you may
|
||||
| indicate that via the expire_on_close configuration option.
|
||||
|
|
||||
*/
|
||||
|
||||
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||
|
||||
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Encryption
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to easily specify that all of your session data
|
||||
| should be encrypted before it's stored. All encryption is performed
|
||||
| automatically by Laravel and you may use the session like normal.
|
||||
|
|
||||
*/
|
||||
|
||||
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session File Location
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the "file" session driver, the session files are placed
|
||||
| on disk. The default storage location is defined here; however, you
|
||||
| are free to provide another location where they should be stored.
|
||||
|
|
||||
*/
|
||||
|
||||
'files' => storage_path('framework/sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Connection
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" or "redis" session drivers, you may specify a
|
||||
| connection that should be used to manage these sessions. This should
|
||||
| correspond to a connection in your database configuration options.
|
||||
|
|
||||
*/
|
||||
|
||||
'connection' => env('SESSION_CONNECTION'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" session driver, you may specify the table to
|
||||
| be used to store sessions. Of course, a sensible default is defined
|
||||
| for you; however, you're welcome to change this to another table.
|
||||
|
|
||||
*/
|
||||
|
||||
'table' => env('SESSION_TABLE', 'sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using one of the framework's cache driven session backends, you may
|
||||
| define the cache store which should be used to store the session data
|
||||
| between requests. This must match one of your defined cache stores.
|
||||
|
|
||||
| Affects: "dynamodb", "memcached", "redis"
|
||||
|
|
||||
*/
|
||||
|
||||
'store' => env('SESSION_STORE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Sweeping Lottery
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Some session drivers must manually sweep their storage location to get
|
||||
| rid of old sessions from storage. Here are the chances that it will
|
||||
| happen on a given request. By default, the odds are 2 out of 100.
|
||||
|
|
||||
*/
|
||||
|
||||
'lottery' => [2, 100],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may change the name of the session cookie that is created by
|
||||
| the framework. Typically, you should not need to change this value
|
||||
| since doing so does not grant a meaningful security improvement.
|
||||
|
|
||||
*/
|
||||
|
||||
'cookie' => env(
|
||||
'SESSION_COOKIE',
|
||||
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
|
||||
),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The session cookie path determines the path for which the cookie will
|
||||
| be regarded as available. Typically, this will be the root path of
|
||||
| your application, but you're free to change this when necessary.
|
||||
|
|
||||
*/
|
||||
|
||||
'path' => env('SESSION_PATH', '/'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the domain and subdomains the session cookie is
|
||||
| available to. By default, the cookie will be available to the root
|
||||
| domain and all subdomains. Typically, this shouldn't be changed.
|
||||
|
|
||||
*/
|
||||
|
||||
'domain' => env('SESSION_DOMAIN'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTPS Only Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By setting this option to true, session cookies will only be sent back
|
||||
| to the server if the browser has a HTTPS connection. This will keep
|
||||
| the cookie from being sent to you when it can't be done securely.
|
||||
|
|
||||
*/
|
||||
|
||||
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTP Access Only
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will prevent JavaScript from accessing the
|
||||
| value of the cookie and the cookie will only be accessible through
|
||||
| the HTTP protocol. It's unlikely you should disable this option.
|
||||
|
|
||||
*/
|
||||
|
||||
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Same-Site Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines how your cookies behave when cross-site requests
|
||||
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||
| will set this value to "lax" to permit secure cross-site requests.
|
||||
|
|
||||
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||
|
|
||||
| Supported: "lax", "strict", "none", null
|
||||
|
|
||||
*/
|
||||
|
||||
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Partitioned Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will tie the cookie to the top-level site for
|
||||
| a cross-site context. Partitioned cookies are accepted by the browser
|
||||
| when flagged "secure" and the Same-Site attribute is set to "none".
|
||||
|
|
||||
*/
|
||||
|
||||
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||
|
||||
];
|
||||
1
database/.gitignore
vendored
Normal file
1
database/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.sqlite*
|
||||
46
database/factories/UserFactory.php
Normal file
46
database/factories/UserFactory.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'no_pekerja' => fake()->unique()->numerify('#####'),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
// public function unverified(): static
|
||||
// {
|
||||
// return $this->state(fn (array $attributes) => [
|
||||
// 'email_verified_at' => null,
|
||||
// ]);
|
||||
// }
|
||||
}
|
||||
48
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
48
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique()->nullable();
|
||||
$table->string('password');
|
||||
$table->enum('role', ['admin','staff'])->default('staff');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up()
|
||||
{
|
||||
Schema::create('surveys', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->onDelete('cascade'); // for admin
|
||||
$table->string('title');
|
||||
$table->date('date')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('surveys');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
|
||||
public function up()
|
||||
{
|
||||
Schema::create('sections', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('survey_id')->constrained('surveys')->onDelete('cascade');
|
||||
$table->string('title');
|
||||
$table->text('description')->nullable();
|
||||
$table->integer('order')->default(0); // Untuk urutan Bahagian A, B, C
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sections');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
|
||||
public function up()
|
||||
{
|
||||
Schema::create('questions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('section_id')->constrained('sections')->onDelete('cascade');
|
||||
$table->text('question_text');
|
||||
$table->string('type'); // 'radio', 'text', 'checkbox'
|
||||
$table->integer('order')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('questions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
|
||||
public function up()
|
||||
{
|
||||
Schema::create('options', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('question_id')->constrained('questions')->onDelete('cascade');
|
||||
$table->string('option_text');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('options');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('responses', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('survey_id')->constrained('surveys')->onDelete('cascade');
|
||||
$table->foreignId('user_id')->nullable()->constrained('users');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('responses');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('answers', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('response_id')->constrained('responses')->onDelete('cascade');
|
||||
$table->foreignId('question_id')->constrained('questions')->onDelete('cascade');
|
||||
$table->text('answer_text');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('answers');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('questions', function (Blueprint $table) {
|
||||
$table->boolean('allow_other_option')->default(false)->after('type');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('questions', function (Blueprint $table) {
|
||||
$table->dropColumn('allow_other_option');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('surveys', function (Blueprint $table) {
|
||||
$table->text('ulasan')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('surveys', function (Blueprint $table) {
|
||||
$table->dropColumn('ulasan');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('responses', function (Blueprint $table) {
|
||||
$table->foreignId('user_id')->nullable()->change();
|
||||
|
||||
$table->string('respondent_name')->nullable();
|
||||
$table->string('respondent_no_pekerja')->nullable();
|
||||
$table->string('respondent_jabatan')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('responses', function (Blueprint $table) {
|
||||
//
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('jabatan')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('jabatan');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('surveys', function (Blueprint $table) {
|
||||
$table->uuid('uuid')->after('id')->nullable();
|
||||
});
|
||||
|
||||
// Backfill existing records
|
||||
\Illuminate\Support\Facades\DB::table('surveys')->get()->each(function ($survey) {
|
||||
\Illuminate\Support\Facades\DB::table('surveys')
|
||||
->where('id', $survey->id)
|
||||
->update(['uuid' => (string) \Illuminate\Support\Str::uuid()]);
|
||||
});
|
||||
|
||||
// Make it non-nullable (optional, but good practice if driver supports it easily)
|
||||
// For portability/safety without doctrine, we might just leave it nullable but enforce in app.
|
||||
// Or if we are sure, we can try change(). Let's stick to nullable for now to avoid dependency issues,
|
||||
// but creating it unique is important.
|
||||
|
||||
Schema::table('surveys', function (Blueprint $table) {
|
||||
// Adding unique index after population
|
||||
$table->unique('uuid');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('surveys', function (Blueprint $table) {
|
||||
$table->dropColumn('uuid');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('surveys', function (Blueprint $table) {
|
||||
$table->text('perincian')->nullable()->after('date');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('surveys', function (Blueprint $table) {
|
||||
$table->dropColumn('perincian');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
|
||||
public function up()
|
||||
{
|
||||
if (!Schema::hasColumn('users', 'no_pekerja')) {
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('no_pekerja')->unique()->after('name');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('no_pekerja');
|
||||
$table->dropUnique(['no_pekerja']);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
};
|
||||
20
database/seeders/AdminUserSeeder.php
Normal file
20
database/seeders/AdminUserSeeder.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class AdminUserSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
User::create([
|
||||
'name' => 'Nur Batrisyia',
|
||||
'no_pekerja' => '2023613838',
|
||||
'email' => 'admin@mbip.gov.my',
|
||||
'password' => Hash::make('password'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
24
database/seeders/DatabaseSeeder.php
Normal file
24
database/seeders/DatabaseSeeder.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
use WithoutModelEvents;
|
||||
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
]);
|
||||
}
|
||||
}
|
||||
4
desktop.ini
Normal file
4
desktop.ini
Normal file
@@ -0,0 +1,4 @@
|
||||
[ViewState]
|
||||
Mode=
|
||||
Vid=
|
||||
FolderType=Generic
|
||||
17
package.json
Normal file
17
package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://www.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"axios": "^1.11.0",
|
||||
"concurrently": "^9.0.1",
|
||||
"laravel-vite-plugin": "^2.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vite": "^7.0.7"
|
||||
}
|
||||
}
|
||||
0
paragraph(rand(2
Normal file
0
paragraph(rand(2
Normal file
35
phpunit.xml
Normal file
35
phpunit.xml
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory>tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory>app</directory>
|
||||
</include>
|
||||
</source>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="PULSE_ENABLED" value="false"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
25
public/.htaccess
Normal file
25
public/.htaccess
Normal file
@@ -0,0 +1,25 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews -Indexes
|
||||
</IfModule>
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Handle Authorization Header
|
||||
RewriteCond %{HTTP:Authorization} .
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
|
||||
# Handle X-XSRF-Token Header
|
||||
RewriteCond %{HTTP:x-xsrf-token} .
|
||||
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
||||
|
||||
# Redirect Trailing Slashes If Not A Folder...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ %1 [L,R=301]
|
||||
|
||||
# Send Requests To Front Controller...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
</IfModule>
|
||||
23
public/css/adminHeader.css
Normal file
23
public/css/adminHeader.css
Normal file
@@ -0,0 +1,23 @@
|
||||
.admin-header-box {
|
||||
background: linear-gradient(45deg, #4e73df, #224abe);
|
||||
color: white;
|
||||
padding: 1.5rem !important;
|
||||
border-radius: 1rem !important;
|
||||
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .075) !important;
|
||||
margin-bottom: 2rem !important;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.admin-header-box h4 {
|
||||
font-weight: 700 !important;
|
||||
margin-bottom: 0 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.admin-header-box p {
|
||||
margin-bottom: 0 !important;
|
||||
opacity: 0.75;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
37
public/css/app.css
Normal file
37
public/css/app.css
Normal file
@@ -0,0 +1,37 @@
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.col-auto-fixed {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
color: white;
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover,
|
||||
.sidebar .nav-link.active {
|
||||
background-color: #495057;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar .btn-logout {
|
||||
color: white;
|
||||
border: 1px solid white;
|
||||
padding: 0.5rem 1rem;
|
||||
width: calc(100% - 2rem);
|
||||
margin: 1rem 1rem 2rem 1rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
}
|
||||
33
public/css/dashboardAdmin.css
Normal file
33
public/css/dashboardAdmin.css
Normal file
@@ -0,0 +1,33 @@
|
||||
.dashboard-card {
|
||||
transition: transform 0.2s;
|
||||
border: none !important;
|
||||
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .075) !important;
|
||||
}
|
||||
|
||||
.dashboard-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.display-stat {
|
||||
font-size: 4rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Gradient Utilities for Cards */
|
||||
.bg-gradient-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
||||
}
|
||||
|
||||
.bg-gradient-success {
|
||||
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%) !important;
|
||||
}
|
||||
|
||||
.bg-gradient-danger {
|
||||
background: linear-gradient(135deg, #ee0979 0%, #ff6a00 100%) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.display-stat {
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
217
public/css/login.css
Normal file
217
public/css/login.css
Normal file
@@ -0,0 +1,217 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: url('/images/mbipPlace.jpeg') center/cover no-repeat;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
max-width: 1100px;
|
||||
width: 100%;
|
||||
background: rgba(88, 70, 120, 0.4);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 40px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Left - Login Form */
|
||||
.form-section {
|
||||
padding: 60px 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo-mbip {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 120px;
|
||||
height: auto;
|
||||
margin-bottom: 40px;
|
||||
background: white;
|
||||
padding: 10px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.form-section h2 {
|
||||
color: white;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-section p {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: 35px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: block;
|
||||
color: white;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
width: 100%;
|
||||
padding: 16px 20px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
color: white;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.input-group input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.input-group input:focus {
|
||||
outline: none;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.btn-signin {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #fbbf24 0%, #fb923c 100%);
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
color: #1f2937;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.btn-signin:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(251, 191, 36, 0.4);
|
||||
}
|
||||
|
||||
/* Right - Content Section */
|
||||
.content-section {
|
||||
padding: 60px 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.5) 100%);
|
||||
}
|
||||
|
||||
.content-title {
|
||||
color: white;
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.content-desc {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
width: 500px;
|
||||
height: 350px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.image-placeholder img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* Alert */
|
||||
.alert {
|
||||
padding: 12px 20px;
|
||||
border-radius: 50px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 0.85rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: #86efac;
|
||||
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #fca5a5;
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 968px) {
|
||||
.login-box {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.form-section {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
|
||||
.form-section h2 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.content-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
28
public/css/responsesAdmin.css
Normal file
28
public/css/responsesAdmin.css
Normal file
@@ -0,0 +1,28 @@
|
||||
.response-survey-item {
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid #e8e8e8;
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.response-survey-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, .08) !important;
|
||||
border-color: #d0d0d0;
|
||||
}
|
||||
|
||||
.icon-box-stat {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.response-survey-item h6 {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.3;
|
||||
font-weight: 600;
|
||||
}
|
||||
15
public/css/statisticsAdmin.css
Normal file
15
public/css/statisticsAdmin.css
Normal file
@@ -0,0 +1,15 @@
|
||||
.chart-container-box {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.other-answer-item {
|
||||
background-color: #f8f9fa !important;
|
||||
color: #6c757d !important;
|
||||
font-size: 0.875rem !important;
|
||||
}
|
||||
|
||||
.stat-card-number {
|
||||
font-weight: 700 !important;
|
||||
color: #333 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
21
public/css/surveysAdmin.css
Normal file
21
public/css/surveysAdmin.css
Normal file
@@ -0,0 +1,21 @@
|
||||
.survey-table-card {
|
||||
border: none !important;
|
||||
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .075) !important;
|
||||
border-radius: 1rem !important;
|
||||
}
|
||||
|
||||
.survey-table-card .card-header {
|
||||
border-radius: 1rem 1rem 0 0 !important;
|
||||
}
|
||||
|
||||
.survey-title-container a {
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.survey-title-container a:hover {
|
||||
color: #4e73df !important;
|
||||
}
|
||||
|
||||
.copy-btn-success {
|
||||
color: #1cc88a !important;
|
||||
}
|
||||
28
public/css/ulasanAdmin.css
Normal file
28
public/css/ulasanAdmin.css
Normal file
@@ -0,0 +1,28 @@
|
||||
.ulasan-hover-effect {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.ulasan-hover-effect:hover {
|
||||
transform: translateY(-3px);
|
||||
background-color: #fdfdfd !important;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.d-print-none {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.col-6 {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
break-inside: avoid;
|
||||
border: 1px solid #eee !important;
|
||||
}
|
||||
|
||||
body {
|
||||
background: white !important;
|
||||
}
|
||||
}
|
||||
17
public/css/usersAdmin.css
Normal file
17
public/css/usersAdmin.css
Normal file
@@ -0,0 +1,17 @@
|
||||
.user-table-card {
|
||||
border: none !important;
|
||||
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .075) !important;
|
||||
border-radius: 1rem !important;
|
||||
}
|
||||
|
||||
.user-table-card .card-body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.badge-admin {
|
||||
background-color: #36b9cc !important;
|
||||
}
|
||||
|
||||
.badge-staff {
|
||||
background-color: #858796 !important;
|
||||
}
|
||||
0
public/favicon.ico
Normal file
0
public/favicon.ico
Normal file
BIN
public/images/mbip.png
Normal file
BIN
public/images/mbip.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 230 KiB |
BIN
public/images/mbip2.jpeg
Normal file
BIN
public/images/mbip2.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
BIN
public/images/mbipPlace.jpeg
Normal file
BIN
public/images/mbipPlace.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 488 KiB |
BIN
public/images/mbippp.jpg
Normal file
BIN
public/images/mbippp.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 914 KiB |
20
public/index.php
Normal file
20
public/index.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Determine if the application is in maintenance mode...
|
||||
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||
require $maintenance;
|
||||
}
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the request...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||
|
||||
$app->handleRequest(Request::capture());
|
||||
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
11
resources/css/app.css
Normal file
11
resources/css/app.css
Normal file
@@ -0,0 +1,11 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
@source '../**/*.blade.php';
|
||||
@source '../**/*.js';
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
1
resources/js/app.js
Normal file
1
resources/js/app.js
Normal file
@@ -0,0 +1 @@
|
||||
import './bootstrap';
|
||||
4
resources/js/bootstrap.js
vendored
Normal file
4
resources/js/bootstrap.js
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
170
resources/views/admin/dashboard.blade.php
Normal file
170
resources/views/admin/dashboard.blade.php
Normal file
@@ -0,0 +1,170 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@push('styles')
|
||||
<link rel="stylesheet" href="{{ asset('css/adminHeader.css') }}">
|
||||
<link rel="stylesheet" href="{{ asset('css/dashboardAdmin.css') }}">
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<div class="admin-header-box mb-4">
|
||||
<div>
|
||||
<h4>Dashboard</h4>
|
||||
<p>Selamat Datang, <strong>{{ auth()->user()->name }}</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-md-4">
|
||||
<div class="card dashboard-card bg-gradient-primary h-100">
|
||||
<div class="card-body text-white p-4">
|
||||
<h6 class="text-white-50 mb-3 fw-normal">Jumlah borang</h6>
|
||||
<h1 class="display-stat mb-0">{{ $totalSurveys }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card dashboard-card bg-gradient-success h-100">
|
||||
<div class="card-body text-white p-4">
|
||||
<h6 class="text-white-50 mb-3 fw-normal">Jumlah respons</h6>
|
||||
<h1 class="display-stat mb-0">{{ $totalResponses }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card dashboard-card bg-gradient-danger h-100">
|
||||
<div class="card-body text-white p-4">
|
||||
<h6 class="text-white-50 mb-3 fw-normal">Jumlah pengguna</h6>
|
||||
<h1 class="display-stat mb-0">{{ $totalUsers }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- recent survey table --}}
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom py-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0 fw-bold">Borang Terkini</h5>
|
||||
<a href="{{ route('admin.surveys.index') }}" class="btn btn-sm btn-outline-primary">
|
||||
Lihat Semua <i class="bi bi-arrow-right ms-1"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="ps-4">Tajuk</th>
|
||||
<th>Dicipta oleh</th>
|
||||
<th>Respons</th>
|
||||
<th>Tarikh</th>
|
||||
<th class="pe-4">Tindakan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($recentSurveys as $survey)
|
||||
<tr>
|
||||
<td class="ps-4">
|
||||
<span class="fw-semibold text-primary" title="{{ $survey->title }}">
|
||||
{{ Str::limit($survey->title, 50) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ $survey->user->name }}</td>
|
||||
<td>
|
||||
<a href="{{ route('admin.responses.respondents', $survey) }}" class="text-decoration-none">
|
||||
<span class="badge bg-success">{{ $survey->responses->count() }} respons</span>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-muted">{{ $survey->created_at->format('d/m/Y') }}</td>
|
||||
<td class="pe-4">
|
||||
<a href="{{ route('admin.surveys.edit', $survey->id) }}" class="btn btn-sm btn-outline-secondary" title="Edit">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form action="{{ route('admin.surveys.destroy', $survey->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Adakah anda pasti mahu memadam borang ini?');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Padam">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-4 text-muted">
|
||||
<i class="bi bi-inbox display-4 d-block mb-2"></i>
|
||||
Tiada borang lagi
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mt-4">
|
||||
{{-- Ulasan Terkini --}}
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header bg-white border-bottom py-3">
|
||||
<h5 class="mb-0 fw-bold">Ulasan Terkini</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<ul class="list-group list-group-flush">
|
||||
@forelse($recentReviews as $review)
|
||||
<li class="list-group-item py-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<span class="fw-semibold text-truncate" style="max-width: 70%;">{{ $review->title }}</span>
|
||||
<small class="text-muted">{{ $review->updated_at->format('d/m/Y') }}</small>
|
||||
</div>
|
||||
<p class="mb-0 text-muted small text-truncate">{{ $review->ulasan }}</p>
|
||||
</li>
|
||||
@empty
|
||||
<li class="list-group-item text-center py-4 text-muted">Tiada ulasan lagi</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- User Terkini --}}
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header bg-white border-bottom py-3">
|
||||
<h5 class="mb-0 fw-bold">Pengguna Terkini</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<ul class="list-group list-group-flush">
|
||||
@forelse($recentUsers as $user)
|
||||
<li class="list-group-item py-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="bg-light rounded-circle p-2 me-3 text-primary">
|
||||
<i class="bi bi-person"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-0 fw-semibold">{{ $user->name }}</h6>
|
||||
<div class="small text-muted">
|
||||
<span>{{ $user->jabatan ?? '-' }}</span> |
|
||||
<span>No Pekerja: {{ $user->no_pekerja ?? '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<span class="badge bg-{{ $user->role == 'admin' ? 'info' : 'secondary' }} rounded-pill">{{ ucfirst($user->role) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@empty
|
||||
<li class="list-group-item text-center py-4 text-muted">Tiada pengguna lagi</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
41
resources/views/admin/responses/detail.blade.php
Normal file
41
resources/views/admin/responses/detail.blade.php
Normal file
@@ -0,0 +1,41 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="card p-2 shadow-sm border-0 mb-3" style="border-radius: 15px;">
|
||||
<h5 class="mb-3 text-success">Jawapan Responden</h5>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<small class="text-muted d-block">Nama</small>
|
||||
<strong>{{ $response->respondent_name ?? '-' }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 border-start border-end">
|
||||
<small class="text-muted d-block">No. Pekerja</small>
|
||||
<strong>{{ $response->respondent_no_pekerja ?? '-' }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<small class="text-muted d-block">Jabatan</small>
|
||||
<strong>{{ $response->respondent_jabatan ?? '-' }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 px-2">
|
||||
<p class="mb-0">
|
||||
<span class="fw-bold text-primary">{{ $response->survey->title }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
@foreach($response->answers as $answer)
|
||||
<div class="mb-3">
|
||||
<strong>{{ $answer->question->question_text }}</strong>
|
||||
<p>{{ $answer->answer_text }}</p>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endsection
|
||||
61
resources/views/admin/responses/list.blade.php
Normal file
61
resources/views/admin/responses/list.blade.php
Normal file
@@ -0,0 +1,61 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@push('styles')
|
||||
<link rel="stylesheet" href="{{ asset('css/adminHeader.css') }}">
|
||||
<link rel="stylesheet" href="{{ asset('css/responsesAdmin.css') }}">
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="admin-header-box">
|
||||
<div>
|
||||
<h4>Senarai Borang & Responden</h4>
|
||||
<p>Klik pada butang tersebut untuk melihat senarai dan jawapan responden.</p>
|
||||
</div>
|
||||
|
||||
<form method="GET" action="{{ route('admin.responses.list') }}" style="max-width: 300px; width: 100%;">
|
||||
<div class="input-group input-group-sm shadow-sm">
|
||||
<input type="text" name="search" value="{{ request('search') }}" class="form-control border-0 ps-3" placeholder="Cari tajuk..." style="border-radius: 20px 0 0 20px;">
|
||||
<button class="btn btn-light border-0 px-3" type="submit" style="color: #4e73df;">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
@if(request('search'))
|
||||
<a href="{{ route('admin.responses.list') }}" class="btn btn-light border-0 border-start px-2" style="border-radius: 0 20px 20px 0;">
|
||||
<i class="bi bi-arrow-counterclockwise"></i>
|
||||
</a>
|
||||
@else
|
||||
<div class="bg-light px-2" style="border-radius: 0 20px 20px 0;"></div>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
@forelse($surveys as $survey)
|
||||
<div class="col-md-6 col-lg-4 d-flex">
|
||||
<div class="response-survey-item d-flex align-items-center justify-content-between p-3 shadow-sm w-100">
|
||||
<div class="d-flex align-items-center flex-grow-1 me-3">
|
||||
<div class="icon-box-stat me-3">
|
||||
<span class="fw-bold text-secondary">#{{ $loop->iteration }}</span>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-1 text-dark text-uppercase">{{ $survey->title }}</h6>
|
||||
<small class="text-muted">{{ $survey->responses_count }} respons</small>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ route('admin.responses.respondents', $survey->id) }}" class="btn btn-primary btn-sm rounded-2 px-3">
|
||||
<i class="bi bi-box-arrow-up-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="col-12 text-center py-5">
|
||||
<div class="text-muted">
|
||||
<i class="bi bi-inbox fs-1 d-block mb-2 opacity-25"></i>
|
||||
<p class="small">Tiada borang ditemui.</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
66
resources/views/admin/responses/respondents.blade.php
Normal file
66
resources/views/admin/responses/respondents.blade.php
Normal file
@@ -0,0 +1,66 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@push('styles')
|
||||
<link rel="stylesheet" href="{{ asset('css/adminHeader.css') }}">
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="admin-header-box mb-4">
|
||||
<div>
|
||||
<h4>Responden: {{ $survey->title }}</h4>
|
||||
<p>Senarai pengguna yang telah menjawab borang ini.</p>
|
||||
</div>
|
||||
<a href="{{ route('admin.responses.list') }}" class="btn btn-sm btn-light px-3 fw-bold text-primary">
|
||||
<i class="bi bi-arrow-left"></i> Kembali
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">Jumlah Responden: {{ $responses->count() }}</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if($responses->isEmpty())
|
||||
<div class="alert alert-info mb-0 p-4">
|
||||
Belum ada responden yang menjawab borang ini.
|
||||
</div>
|
||||
@else
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Responden</th>
|
||||
<th>Nombor Pekerja</th>
|
||||
<th>Tarikh Jawab</th>
|
||||
<th>Tindakan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($responses as $index => $response)
|
||||
<tr>
|
||||
<td>{{ $index + 1 }}</td>
|
||||
<td>
|
||||
{{-- Klik nama/butang untuk ke halaman detail.blade.php --}}
|
||||
<a href="{{ route('admin.responses.detail', $response->id) }}" class="text-primary fw-bold">
|
||||
{{ $response->respondent_name ?? '-' }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ $response->respondent_no_pekerja ?? '-' }}</td>
|
||||
<td>{{ $response->created_at->format('d M Y') }}</td>
|
||||
<td>
|
||||
<a href="{{ route('admin.responses.detail', $response->id) }}" class="btn btn-sm btn-info text-white">
|
||||
Lihat Jawapan
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
342
resources/views/admin/surveys/create.blade.php
Normal file
342
resources/views/admin/surveys/create.blade.php
Normal file
@@ -0,0 +1,342 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@push('styles')
|
||||
<link rel="stylesheet" href="{{ asset('css/adminHeader.css') }}">
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
<strong>Ralat! Sila semak input anda:</strong>
|
||||
<ul>
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="container">
|
||||
<div class="admin-header-box mb-4">
|
||||
<div>
|
||||
<h4>Daftar Soal Selidik</h4>
|
||||
<p>Sila isi maklumat borang di bawah.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (session('error'))
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<strong>Ralat Sistem:</strong> {{ session('error') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('admin.surveys.store') }}" id="survey-builder-form">
|
||||
@csrf
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body p-4">
|
||||
<div class="row">
|
||||
<div class="col-md-8 mb-3">
|
||||
<label for="title" class="form-label fw-bold">Tajuk Soal Selidik</label>
|
||||
<input type="text" name="title" id="title" class="form-control" placeholder="eg: Sambutan Maulidur Rasul" value="{{ old('title') }}" required>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="date" class="form-label fw-bold">Tarikh</label>
|
||||
<input type="date" name="date" id="date" class="form-control" value="{{ old('date') }}" required>
|
||||
</div>
|
||||
<div class="col-md-12 mb-3">
|
||||
<label for="perincian" class="form-label fw-bold">Perincian</label>
|
||||
<textarea name="perincian" id="perincian" class="form-control" rows="3" placeholder="Terangkan tujuan borang ini (Pilihan)">{{ old('perincian') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sections-container"></div>
|
||||
|
||||
<button type="button" class="btn btn-outline-primary" id="add-section-btn">+ Tambah Bahagian</button>
|
||||
|
||||
<div class="text-end mt-4">
|
||||
<a href="{{ route('admin.surveys.index') }}" class="btn btn-secondary btn-lg">Batal</a>
|
||||
<button type="submit" class="btn btn-success btn-lg">Daftar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<template id="section-template">
|
||||
<div class="card shadow-sm mb-4 section-block">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0 section-title">Bahagian</h5>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger remove-section-btn">Hapus Bahagian</button>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<input type="text" name="sections[0][title]" class="form-control section-title-input" placeholder="Tajuk Bahagian (cth: Maklumat Peribadi)" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<input type="text" name="sections[0][description]" class="form-control" placeholder="Keterangan (Pilihan)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="questions-container"></div>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-info add-question-btn">+ Tambah soalan</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="question-template">
|
||||
<div class="border rounded p-3 mb-3 question-block bg-white">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label fw-bold question-title">Soalan 1</label>
|
||||
<div>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary type-selector" data-type="radio">Radio</button>
|
||||
<button type="button" class="btn btn-outline-secondary type-selector" data-type="text">Text</button>
|
||||
<button type="button" class="btn btn-outline-secondary type-selector" data-type="checkbox">Checkbox</button>
|
||||
</div>
|
||||
<input type="hidden" name="sections[0][questions][0][type]" class="question-type-input" value="radio">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger ms-2 remove-question-btn">Hapus</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="text" name="sections[0][questions][0][text]" class="form-control mb-3 question-text-input" placeholder="Isi soalan di sini" required>
|
||||
|
||||
<div class="form-check allow-other-container mb-3" data-allowed-types="radio,checkbox">
|
||||
<input class="form-check-input allow-other-option-checkbox" type="checkbox"
|
||||
value="1"
|
||||
name="sections[0][questions][0][allow_other_option]">
|
||||
<label class="form-check-label" for="flexCheckDefault">
|
||||
Lain-lain
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="options-container">
|
||||
<label class="form-label">Jawapan Pilihan</label>
|
||||
<button type="button" class="btn btn-sm btn-link add-option-btn p-0">+ Tambah pilihan</button>
|
||||
</div>
|
||||
|
||||
<div class="text-placeholder-container" style="display:none;">
|
||||
<input type="text" class="form-control bg-light" placeholder="Ruang responden menjawab" disabled>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="option-template">
|
||||
<div class="input-group mb-2 option-group">
|
||||
<input type="text" name="sections[0][questions][0][options][]" class="form-control option-input" placeholder="Pilihan jawapan" required>
|
||||
<button type="button" class="btn btn-outline-danger remove-option-btn">Hapus</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const sectionsContainer = document.getElementById('sections-container');
|
||||
const addSectionBtn = document.getElementById('add-section-btn');
|
||||
|
||||
const sectionTemplate = document.getElementById('section-template');
|
||||
const questionTemplate = document.getElementById('question-template');
|
||||
const optionTemplate = document.getElementById('option-template');
|
||||
|
||||
let sectionCounter = 0;
|
||||
|
||||
function addSection() {
|
||||
const sectionIndex = sectionCounter++;
|
||||
const newSection = sectionTemplate.content.cloneNode(true).firstElementChild;
|
||||
|
||||
newSection.dataset.sectionIndex = sectionIndex;
|
||||
newSection.querySelector('.section-title').textContent = `Bahagian`;
|
||||
|
||||
|
||||
newSection.querySelectorAll('[name]').forEach(input => {
|
||||
input.name = input.name.replace('sections[0]', `sections[${sectionIndex}]`);
|
||||
});
|
||||
|
||||
addQuestion(newSection.querySelector('.questions-container'), sectionIndex);
|
||||
|
||||
sectionsContainer.appendChild(newSection);
|
||||
reindexQuestions();
|
||||
}
|
||||
|
||||
function addQuestion(questionsContainer, sectionIndex) {
|
||||
const questionIndex = questionsContainer.querySelectorAll('.question-block').length;
|
||||
const newQuestion = questionTemplate.content.cloneNode(true).firstElementChild;
|
||||
|
||||
newQuestion.dataset.questionIndex = questionIndex;
|
||||
newQuestion.querySelector('.question-title').textContent = `Soalan ${questionIndex + 1}`;
|
||||
|
||||
newQuestion.querySelectorAll('[name]').forEach(input => {
|
||||
input.name = input.name
|
||||
.replace('sections[0]', `sections[${sectionIndex}]`)
|
||||
.replace('[questions][0]', `[questions][${questionIndex}]`);
|
||||
});
|
||||
|
||||
newQuestion.querySelector('.type-selector[data-type="radio"]').classList.add('active', 'btn-primary');
|
||||
newQuestion.querySelector('.question-type-input').value = 'radio';
|
||||
|
||||
const otherCheckbox = newQuestion.querySelector('.allow-other-option-checkbox');
|
||||
otherCheckbox.name = `sections[${sectionIndex}][questions][${questionIndex}][allow_other_option]`;
|
||||
otherCheckbox.checked = false; // Pastikan bermula sebagai tidak dicentang
|
||||
otherCheckbox.removeAttribute('checked')
|
||||
|
||||
const optionsContainer = newQuestion.querySelector('.options-container');
|
||||
addOption(optionsContainer, sectionIndex, questionIndex);
|
||||
addOption(optionsContainer, sectionIndex, questionIndex);
|
||||
|
||||
questionsContainer.appendChild(newQuestion);
|
||||
reindexQuestions();
|
||||
}
|
||||
|
||||
function addOption(optionsContainer, sectionIndex, questionIndex) {
|
||||
const newOption = optionTemplate.content.cloneNode(true).firstElementChild;
|
||||
newOption.querySelector('.option-input').name = `sections[${sectionIndex}][questions][${questionIndex}][options][]`;
|
||||
|
||||
const addBtn = optionsContainer.querySelector('.add-option-btn');
|
||||
optionsContainer.insertBefore(newOption, addBtn);
|
||||
}
|
||||
|
||||
function reindexQuestions() {
|
||||
sectionsContainer.querySelectorAll('.section-block').forEach((section, sIdx) => {
|
||||
section.dataset.sectionIndex = sIdx;
|
||||
section.querySelectorAll('.section-title-input').forEach(input => {
|
||||
input.name = input.name.replace(/sections\[\d+\]/, `sections[${sIdx}]`);
|
||||
});
|
||||
|
||||
section.querySelectorAll('.question-block').forEach((q, qIdx) => {
|
||||
q.dataset.questionIndex = qIdx;
|
||||
q.querySelector('.question-title').textContent = `Soalan ${qIdx + 1}`;
|
||||
q.querySelectorAll('[name]').forEach(input => {
|
||||
input.name = input.name
|
||||
.replace(/sections\[\d+\]/, `sections[${sIdx}]`)
|
||||
.replace(/questions\[\d+\]/, `questions[${qIdx}]`);
|
||||
});
|
||||
|
||||
const type = q.querySelector('.question-type-input').value;
|
||||
const optionsContainer = q.querySelector('.options-container');
|
||||
const textPlaceholder = q.querySelector('.text-placeholder-container');
|
||||
const otherContainer = q.querySelector('.allow-other-container');
|
||||
|
||||
if (type === 'text') {
|
||||
optionsContainer.style.display = 'none';
|
||||
textPlaceholder.style.display = 'block';
|
||||
otherContainer.style.display = 'none'; // Sembunyikan untuk jenis 'text'
|
||||
q.querySelector('.allow-other-option-checkbox').checked = false;
|
||||
q.querySelector('.allow-other-option-checkbox').removeAttribute('checked');
|
||||
|
||||
// Buang input options supaya tidak dihantar jika jenis 'text'
|
||||
// Ini penting, jika tidak, validation akan gagal
|
||||
q.querySelectorAll('.option-group').forEach(group => group.remove());
|
||||
|
||||
} else {
|
||||
optionsContainer.style.display = 'block';
|
||||
textPlaceholder.style.display = 'none';
|
||||
otherContainer.style.display = 'block'; // Tunjukkan untuk jenis 'radio'/'checkbox'
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addSectionBtn.addEventListener('click', addSection);
|
||||
|
||||
sectionsContainer.addEventListener('click', function (e) {
|
||||
if (e.target.classList.contains('add-question-btn')) {
|
||||
const questionsContainer = e.target.closest('.card-body').querySelector('.questions-container');
|
||||
const sectionIndex = parseInt(e.target.closest('.section-block').dataset.sectionIndex);
|
||||
addQuestion(questionsContainer, sectionIndex);
|
||||
}
|
||||
|
||||
if (e.target.classList.contains('add-option-btn')) {
|
||||
const optionsContainer = e.target.closest('.options-container');
|
||||
const questionBlock = e.target.closest('.question-block');
|
||||
const sectionIndex = parseInt(e.target.closest('.section-block').dataset.sectionIndex);
|
||||
const questionIndex = parseInt(questionBlock.dataset.questionIndex);
|
||||
addOption(optionsContainer, sectionIndex, questionIndex);
|
||||
}
|
||||
|
||||
if (e.target.classList.contains('type-selector')) {
|
||||
const type = e.target.dataset.type;
|
||||
const questionBlock = e.target.closest('.question-block');
|
||||
|
||||
questionBlock.querySelectorAll('.type-selector').forEach(btn => btn.classList.remove('active', 'btn-primary'));
|
||||
e.target.classList.add('active', 'btn-primary');
|
||||
|
||||
questionBlock.querySelector('.question-type-input').value = type;
|
||||
|
||||
const optionsContainer = questionBlock.querySelector('.options-container');
|
||||
const textPlaceholder = questionBlock.querySelector('.text-placeholder-container');
|
||||
// BARU: Dapatkan container untuk checkbox 'Lain-lain'
|
||||
const allowOtherContainer = questionBlock.querySelector('.allow-other-container');
|
||||
|
||||
if (type === 'text') {
|
||||
optionsContainer.style.display = 'none';
|
||||
textPlaceholder.style.display = 'block';
|
||||
// BARU: Sembunyikan checkbox 'Lain-lain' untuk jenis 'text'
|
||||
allowOtherContainer.style.display = 'none';
|
||||
questionBlock.querySelector('.allow-other-option-checkbox').checked = false;
|
||||
|
||||
// Remove options
|
||||
optionsContainer.querySelectorAll('.option-group').forEach(group => group.remove());
|
||||
|
||||
} else {
|
||||
optionsContainer.style.display = 'block';
|
||||
textPlaceholder.style.display = 'none';
|
||||
// BARU: Tunjukkan checkbox 'Lain-lain' untuk jenis 'radio'/'checkbox'
|
||||
allowOtherContainer.style.display = 'block';
|
||||
|
||||
// Tambah 2 pilihan jika tiada pilihan (bila tukar dari 'text')
|
||||
if (optionsContainer.querySelectorAll('.option-group').length === 0) {
|
||||
const sectionIndex = parseInt(e.target.closest('.section-block').dataset.sectionIndex);
|
||||
const questionIndex = parseInt(questionBlock.dataset.questionIndex);
|
||||
addOption(optionsContainer, sectionIndex, questionIndex);
|
||||
addOption(optionsContainer, sectionIndex, questionIndex);
|
||||
}
|
||||
}
|
||||
reindexQuestions();
|
||||
}
|
||||
|
||||
if (e.target.closest('.remove-section-btn')) {
|
||||
e.target.closest('.section-block').remove();
|
||||
reindexQuestions();
|
||||
}
|
||||
if (e.target.closest('.remove-question-btn')) {
|
||||
e.target.closest('.question-block').remove();
|
||||
reindexQuestions();
|
||||
}
|
||||
if (e.target.closest('.remove-option-btn')) {
|
||||
e.target.closest('.option-group').remove();
|
||||
reindexQuestions();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('survey-builder-form').addEventListener('submit', function (e) {
|
||||
sectionsContainer.querySelectorAll('.question-block').forEach(q => {
|
||||
const isText = q.querySelector('.question-type-input').value === 'text';
|
||||
|
||||
// 1. Logik untuk soalan 'text' (buang options)
|
||||
if (isText) {
|
||||
q.querySelectorAll('.option-group').forEach(group => group.remove());
|
||||
}
|
||||
|
||||
// 2. Logik untuk checkbox 'allow_other_option'
|
||||
const otherCheckbox = q.querySelector('.allow-other-option-checkbox');
|
||||
if (otherCheckbox) {
|
||||
if (otherCheckbox.checked) {
|
||||
otherCheckbox.value = '1';
|
||||
otherCheckbox.setAttribute('checked', 'checked'); // Pastikan dihantar
|
||||
} else {
|
||||
otherCheckbox.removeAttribute('name');
|
||||
otherCheckbox.removeAttribute('checked');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
addSection();
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
380
resources/views/admin/surveys/edit.blade.php
Normal file
380
resources/views/admin/surveys/edit.blade.php
Normal file
@@ -0,0 +1,380 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@push('styles')
|
||||
<link rel="stylesheet" href="{{ asset('css/adminHeader.css') }}">
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="admin-header-box mb-4">
|
||||
<div>
|
||||
<h4>Edit Soal Selidik</h4>
|
||||
<p>Kemaskini maklumat borang di bawah.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
<strong>Ralat! Sila semak input anda:</strong>
|
||||
<ul>
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (session('error'))
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<strong>Ralat Sistem:</strong> {{ session('error') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('admin.surveys.update', $survey->id) }}" id="survey-builder-form">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body p-4">
|
||||
<div class="row">
|
||||
<div class="col-md-8 mb-3">
|
||||
<label for="title" class="form-label fw-bold">Tajuk Soal Selidik</label>
|
||||
<input type="text" name="title" id="title" class="form-control" placeholder="eg: Sambutan Maulidur Rasul" value="{{ old('title', $survey->title) }}" required>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="date" class="form-label fw-bold">Tarikh</label>
|
||||
<input type="date" name="date" id="date" class="form-control" value="{{ old('date', $survey->date) }}" required>
|
||||
</div>
|
||||
<div class="col-md-12 mb-3">
|
||||
<label for="perincian" class="form-label fw-bold">Perincian</label>
|
||||
<textarea name="perincian" id="perincian" class="form-control" rows="3" placeholder="Terangkan tujuan borang ini (Pilihan)">{{ old('perincian', $survey->perincian) }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sections-container"></div>
|
||||
|
||||
<button type="button" class="btn btn-outline-primary" id="add-section-btn">+ Tambah Seksyen</button>
|
||||
|
||||
<div class="text-end mt-4">
|
||||
<a href="{{ route('admin.surveys.index') }}" class="btn btn-secondary">Batal</a>
|
||||
<button type="submit" class="btn btn-success">Simpan Perubahan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- TEMPLATES --}}
|
||||
<template id="section-template">
|
||||
<div class="card shadow-sm mb-4 section-block">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0 section-title">Bahagian</h5>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger remove-section-btn">Buang Seksyen</button>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<input type="text" name="sections[0][title]" class="form-control section-title-input" placeholder="Tajuk Bahagian (cth: Maklumat Peribadi)" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<input type="text" name="sections[0][description]" class="form-control" placeholder="Optional description">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="questions-container"></div>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-info add-question-btn">+ Tambah soalan</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="question-template">
|
||||
<div class="border rounded p-3 mb-3 question-block bg-white">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label fw-bold question-title">Soalan 1</label>
|
||||
<div>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary type-selector" data-type="radio">Radio</button>
|
||||
<button type="button" class="btn btn-outline-secondary type-selector" data-type="text">Text</button>
|
||||
<button type="button" class="btn btn-outline-secondary type-selector" data-type="checkbox">Checkbox</button>
|
||||
</div>
|
||||
<input type="hidden" name="sections[0][questions][0][type]" class="question-type-input" value="radio">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger ms-2 remove-question-btn">Buang</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="text" name="sections[0][questions][0][text]" class="form-control mb-3 question-text-input" placeholder="Isi soalan di sini" required>
|
||||
|
||||
<div class="form-check allow-other-container mb-3">
|
||||
<input class="form-check-input allow-other-option-checkbox" type="checkbox" value="1" name="sections[0][questions][0][allow_other_option]">
|
||||
<label class="form-check-label">
|
||||
Lain-lain
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="options-container">
|
||||
<label class="form-label">Jawapan Pilihan</label>
|
||||
<button type="button" class="btn btn-sm btn-link add-option-btn p-0">+ Tambah pilihan</button>
|
||||
</div>
|
||||
|
||||
<div class="text-placeholder-container" style="display:none;">
|
||||
<input type="text" class="form-control bg-light" placeholder="Ruang responden menjawab" disabled>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="option-template">
|
||||
<div class="input-group mb-2 option-group">
|
||||
<input type="text" name="sections[0][questions][0][options][]" class="form-control option-input" placeholder="Pilihan jawapan" required>
|
||||
<button type="button" class="btn btn-outline-danger remove-option-btn">Buang</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const sectionsContainer = document.getElementById('sections-container');
|
||||
const addSectionBtn = document.getElementById('add-section-btn');
|
||||
|
||||
const sectionTemplate = document.getElementById('section-template');
|
||||
const questionTemplate = document.getElementById('question-template');
|
||||
const optionTemplate = document.getElementById('option-template');
|
||||
|
||||
let sectionCounter = 0;
|
||||
|
||||
function addSection(data = null) {
|
||||
const sectionIndex = sectionCounter++;
|
||||
const newSection = sectionTemplate.content.cloneNode(true).firstElementChild;
|
||||
newSection.dataset.sectionIndex = sectionIndex;
|
||||
|
||||
const titleInput = newSection.querySelector('.section-title-input');
|
||||
const descInput = newSection.querySelector('input[name$="[description]"]');
|
||||
titleInput.value = data?.title ?? '';
|
||||
descInput.value = data?.description ?? '';
|
||||
newSection.querySelector('.section-title').textContent = data?.title ?? 'Bahagian';
|
||||
|
||||
// Adjust names
|
||||
newSection.querySelectorAll('[name]').forEach(input => {
|
||||
input.name = input.name.replace('sections[0]', `sections[${sectionIndex}]`);
|
||||
});
|
||||
|
||||
const questionsContainer = newSection.querySelector('.questions-container');
|
||||
|
||||
if (data?.questions && data.questions.length) {
|
||||
data.questions.forEach(q => addQuestion(questionsContainer, sectionIndex, q));
|
||||
} else {
|
||||
addQuestion(questionsContainer, sectionIndex);
|
||||
}
|
||||
|
||||
sectionsContainer.appendChild(newSection);
|
||||
reindexQuestions();
|
||||
}
|
||||
|
||||
function addQuestion(questionsContainer, sectionIndex, qdata = null) {
|
||||
const questionIndex = questionsContainer.querySelectorAll('.question-block').length;
|
||||
const newQuestion = questionTemplate.content.cloneNode(true).firstElementChild;
|
||||
newQuestion.dataset.questionIndex = questionIndex;
|
||||
|
||||
newQuestion.querySelector('.question-title').textContent = `Soalan ${questionIndex + 1}`;
|
||||
const qtext = newQuestion.querySelector('.question-text-input');
|
||||
const qtype = newQuestion.querySelector('.question-type-input');
|
||||
const otherCheckbox = newQuestion.querySelector('.allow-other-option-checkbox');
|
||||
|
||||
qtext.value = qdata?.text ?? '';
|
||||
qtype.value = qdata?.type ?? 'radio';
|
||||
|
||||
// BARU: Set status checkbox Lain-lain based on DB data
|
||||
// Check if qdata.allow_other_option is true or '1'
|
||||
if (qdata && (qdata.allow_other_option == 1 || qdata.allow_other_option === true)) {
|
||||
otherCheckbox.checked = true;
|
||||
} else {
|
||||
otherCheckbox.checked = false;
|
||||
}
|
||||
|
||||
newQuestion.querySelectorAll('.type-selector').forEach(btn => {
|
||||
btn.classList.remove('active','btn-primary');
|
||||
if (btn.dataset.type === qtype.value) btn.classList.add('active','btn-primary');
|
||||
});
|
||||
|
||||
newQuestion.querySelectorAll('[name]').forEach(input => {
|
||||
input.name = input.name
|
||||
.replace('sections[0]', `sections[${sectionIndex}]`)
|
||||
.replace('[questions][0]', `[questions][${questionIndex}]`);
|
||||
});
|
||||
|
||||
const optionsContainer = newQuestion.querySelector('.options-container');
|
||||
|
||||
if (qdata?.options && qdata.options.length && qtype.value !== 'text') {
|
||||
qdata.options.forEach(opt => addOption(optionsContainer, sectionIndex, questionIndex, opt.text));
|
||||
} else if (qtype.value !== 'text') {
|
||||
addOption(optionsContainer, sectionIndex, questionIndex);
|
||||
addOption(optionsContainer, sectionIndex, questionIndex);
|
||||
}
|
||||
|
||||
// Initialize display state (options vs text vs other checkbox)
|
||||
toggleQuestionType(newQuestion, qtype.value);
|
||||
|
||||
questionsContainer.appendChild(newQuestion);
|
||||
reindexQuestions();
|
||||
}
|
||||
|
||||
function addOption(optionsContainer, sectionIndex, questionIndex, value = '') {
|
||||
const newOption = optionTemplate.content.cloneNode(true).firstElementChild;
|
||||
const optInput = newOption.querySelector('.option-input');
|
||||
optInput.name = `sections[${sectionIndex}][questions][${questionIndex}][options][]`;
|
||||
optInput.value = value ?? '';
|
||||
const addBtn = optionsContainer.querySelector('.add-option-btn');
|
||||
optionsContainer.insertBefore(newOption, addBtn);
|
||||
}
|
||||
|
||||
function reindexQuestions() {
|
||||
sectionsContainer.querySelectorAll('.section-block').forEach((section, sIdx) => {
|
||||
section.dataset.sectionIndex = sIdx;
|
||||
section.querySelectorAll('.section-title-input').forEach(input => {
|
||||
input.name = input.name.replace(/sections\[\d+\]/, `sections[${sIdx}]`);
|
||||
});
|
||||
|
||||
section.querySelectorAll('.question-block').forEach((q, qIdx) => {
|
||||
q.dataset.questionIndex = qIdx;
|
||||
q.querySelector('.question-title').textContent = `Soalan ${qIdx + 1}`;
|
||||
|
||||
// Reindex all inputs including the new checkbox
|
||||
q.querySelectorAll('[name]').forEach(input => {
|
||||
input.name = input.name
|
||||
.replace(/sections\[\d+\]/, `sections[${sIdx}]`)
|
||||
.replace(/questions\[\d+\]/, `questions[${qIdx}]`);
|
||||
});
|
||||
|
||||
// Reindex dynamic options
|
||||
q.querySelectorAll('.option-input').forEach((opt, optIdx) => {
|
||||
opt.name = `sections[${sIdx}][questions][${qIdx}][options][${optIdx}]`;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toggleQuestionType(questionBlock, type) {
|
||||
const optionsContainer = questionBlock.querySelector('.options-container');
|
||||
const textPlaceholder = questionBlock.querySelector('.text-placeholder-container');
|
||||
const otherContainer = questionBlock.querySelector('.allow-other-container');
|
||||
const otherCheckbox = questionBlock.querySelector('.allow-other-option-checkbox');
|
||||
|
||||
if (type === 'text') {
|
||||
// Text Mode
|
||||
optionsContainer.style.display = 'none';
|
||||
textPlaceholder.style.display = 'block';
|
||||
|
||||
// Hide 'Other' checkbox and uncheck it
|
||||
otherContainer.style.display = 'none';
|
||||
otherCheckbox.checked = false;
|
||||
|
||||
// Remove options inputs
|
||||
optionsContainer.querySelectorAll('.option-input').forEach(i=>i.remove());
|
||||
} else {
|
||||
// Radio/Checkbox Mode
|
||||
optionsContainer.style.display = 'block';
|
||||
textPlaceholder.style.display = 'none';
|
||||
|
||||
// Show 'Other' checkbox
|
||||
otherContainer.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
addSectionBtn.addEventListener('click', () => addSection());
|
||||
|
||||
sectionsContainer.addEventListener('click', function (e) {
|
||||
if (e.target.classList.contains('add-question-btn')) {
|
||||
const questionsContainer = e.target.closest('.card-body').querySelector('.questions-container');
|
||||
const sIdx = parseInt(e.target.closest('.section-block').dataset.sectionIndex);
|
||||
addQuestion(questionsContainer, sIdx);
|
||||
}
|
||||
|
||||
if (e.target.classList.contains('add-option-btn')) {
|
||||
const optionsContainer = e.target.closest('.options-container');
|
||||
const qBlock = e.target.closest('.question-block');
|
||||
const sIdx = parseInt(e.target.closest('.section-block').dataset.sectionIndex);
|
||||
const qIdx = parseInt(qBlock.dataset.questionIndex);
|
||||
addOption(optionsContainer, sIdx, qIdx);
|
||||
}
|
||||
|
||||
if (e.target.classList.contains('type-selector')) {
|
||||
const type = e.target.dataset.type;
|
||||
const qBlock = e.target.closest('.question-block');
|
||||
|
||||
qBlock.querySelectorAll('.type-selector').forEach(btn => btn.classList.remove('active','btn-primary'));
|
||||
e.target.classList.add('active','btn-primary');
|
||||
|
||||
qBlock.querySelector('.question-type-input').value = type;
|
||||
|
||||
// Check if we need to add default options when switching from Text to Radio/Checkbox
|
||||
const optionsContainer = qBlock.querySelector('.options-container');
|
||||
if (type !== 'text' && optionsContainer.querySelectorAll('.option-group').length === 0) {
|
||||
const sIdx = parseInt(qBlock.closest('.section-block').dataset.sectionIndex);
|
||||
const qIdx = parseInt(qBlock.dataset.questionIndex);
|
||||
addOption(optionsContainer, sIdx, qIdx);
|
||||
addOption(optionsContainer, sIdx, qIdx);
|
||||
}
|
||||
|
||||
toggleQuestionType(qBlock, type);
|
||||
reindexQuestions();
|
||||
}
|
||||
|
||||
if (e.target.closest('.remove-section-btn')) {
|
||||
e.target.closest('.section-block').remove();
|
||||
reindexQuestions();
|
||||
}
|
||||
if (e.target.closest('.remove-question-btn')) {
|
||||
e.target.closest('.question-block').remove();
|
||||
reindexQuestions();
|
||||
}
|
||||
if (e.target.closest('.remove-option-btn')) {
|
||||
e.target.closest('.option-group').remove();
|
||||
reindexQuestions();
|
||||
}
|
||||
});
|
||||
|
||||
// Form Submission Logic
|
||||
document.getElementById('survey-builder-form').addEventListener('submit', function () {
|
||||
sectionsContainer.querySelectorAll('.question-block').forEach(q=>{
|
||||
const type = q.querySelector('.question-type-input').value;
|
||||
|
||||
// Handle Options Clean up
|
||||
if (type === 'text') {
|
||||
q.querySelectorAll('.option-input').forEach(i=>i.remove());
|
||||
} else {
|
||||
q.querySelectorAll('.option-input').forEach(i => {
|
||||
if (typeof i.value === 'undefined') i.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
// Handle Other Option Checkbox
|
||||
const otherCheckbox = q.querySelector('.allow-other-option-checkbox');
|
||||
if (otherCheckbox) {
|
||||
if (otherCheckbox.checked) {
|
||||
otherCheckbox.value = '1';
|
||||
} else {
|
||||
// Remove name attribute so it doesn't send "on" or any value if unchecked
|
||||
// This relies on the backend treating missing boolean field as false/0
|
||||
otherCheckbox.removeAttribute('name');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Initial Data Loading
|
||||
const oldSections = @json(old('sections') ?? null);
|
||||
const serverSections = @json($sections_with_questions ?? []);
|
||||
|
||||
const initialSections = oldSections ?? serverSections;
|
||||
|
||||
if (Array.isArray(initialSections) && initialSections.length) {
|
||||
initialSections.forEach(sec => addSection(sec));
|
||||
} else {
|
||||
addSection();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
235
resources/views/admin/surveys/index.blade.php
Normal file
235
resources/views/admin/surveys/index.blade.php
Normal file
@@ -0,0 +1,235 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@push('styles')
|
||||
<link rel="stylesheet" href="{{ asset('css/adminHeader.css') }}">
|
||||
<link rel="stylesheet" href="{{ asset('css/surveysAdmin.css') }}">
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
{{-- Header --}}
|
||||
<div class="admin-header-box">
|
||||
<div>
|
||||
<h4>Senarai Borang soal selidik</h4>
|
||||
<p>Beserta statistik dan analisis Post-Mortem.</p>
|
||||
</div>
|
||||
<a href="{{ route('admin.surveys.create') }}" class="btn btn-sm btn-light px-3 fw-bold text-primary">
|
||||
<i class="bi bi-plus-lg"></i> Daftar soal selidik
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
{{ session('success') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Carian borang --}}
|
||||
<form method="GET" action="{{ route('admin.surveys.index') }}" class="mb-4">
|
||||
<div class="input-group shadow-sm">
|
||||
<input type="text" name="search" value="{{ request('search') }}" class="form-control border-end-0" placeholder="Cari tajuk atau kata kunci...">
|
||||
<button class="btn btn-primary" type="submit"><i class="bi bi-search"></i> </button>
|
||||
<a href="{{ route('admin.surveys.index') }}" class="btn btn-outline-secondary"><i class="bi bi-arrow-counterclockwise"></i></a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="card survey-table-card shadow-sm border-0">
|
||||
<div class="card-header bg-secondary text-white py-3">
|
||||
<h5 class="mb-0 text-center fw-bold">Senarai Soal Selidik</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive-lg">
|
||||
<table class="table table-hover table-striped table-bordered align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="text-center">#</th>
|
||||
<th style="width: 40%;">Tajuk</th>
|
||||
<th style="width: 20%;">Dibuat oleh</th>
|
||||
<th class="text-center">Respons</th>
|
||||
<th>Tarikh</th>
|
||||
<th class="text-center">Stats & Analisis</th>
|
||||
<th class="text-center">Tindakan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($surveys as $index => $survey)
|
||||
<tr>
|
||||
<td class="text-center">{{ $index + 1 }}</td>
|
||||
<td>
|
||||
<div class="fw-semibold text-uppercase text-primary position-relative survey-title-container">
|
||||
<a href="{{ route('surveys.show', $survey) }}"
|
||||
target="_blank"
|
||||
class="text-decoration-none fw-bold text-primary">
|
||||
{{ $survey->title }}
|
||||
</a>
|
||||
<button class="btn btn-link btn-sm p-0 ms-2 text-secondary"
|
||||
onclick="copySurveyLink('{{ $survey->uuid }}')"
|
||||
title="Copy Link">
|
||||
<i class="bi bi-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ $survey->user->name }}</td>
|
||||
<td class="text-center">
|
||||
<span class="badge rounded-pill bg-info text-white px-3">
|
||||
{{ $survey->responses->count() }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ $survey->date }}</td>
|
||||
<td class="text-center">
|
||||
<a href="{{ route('admin.surveys.statistics', $survey->id) }}" class="btn btn-sm btn-info text-white fw-semibold">
|
||||
<i class="bi bi-bar-chart"></i> Graf
|
||||
</a>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-warning fw-semibold" data-bs-toggle="modal" data-bs-target="#modalUlasan{{ $survey->id }}">
|
||||
<i class="bi bi-clipboard-check"></i> Ulasan
|
||||
</button>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-three-dots-vertical"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu shadow border-0">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ route('admin.surveys.edit', $survey->id) }}">
|
||||
<i class="bi bi-pencil me-2 text-primary"></i> Edit Borang
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<form action="{{ route('admin.surveys.destroy', $survey->id) }}" method="POST" onsubmit="return confirm('Padam borang ini?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="dropdown-item text-danger">
|
||||
<i class="bi bi-trash me-2"></i> Hapus
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<div class="modal fade" id="modalUlasan{{ $survey->id }}" tabindex="-1" aria-labelledby="modalLabel{{ $survey->id }}" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow-lg">
|
||||
<div class="modal-header bg-warning">
|
||||
<h5 class="modal-title fw-bold" id="modalLabel{{ $survey->id }}">
|
||||
<i class="bi bi-pencil-square me-2"></i>Ulasan Post-Mortem
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('admin.surveys.update_ulasan', $survey->id) }}" method="POST">
|
||||
@csrf
|
||||
<div class="modal-body py-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Tajuk Borang:</label>
|
||||
<p class="text-muted border-bottom pb-2">{{ $survey->title }}</p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="ulasan" class="form-label fw-bold text-dark">Nota Penambahbaikan:</label>
|
||||
<textarea name="ulasan" class="form-control border-warning shadow-sm" rows="6" placeholder="Tulis ulasan penambahbaikan di sini">{{ $survey->ulasan }}</textarea>
|
||||
<div class="form-text mt-2">
|
||||
<i class="bi bi-info-circle me-1"></i> Perkara yang perlu ditambah baik pada masa akan datang.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light text-end">
|
||||
<button type="button" class="btn btn-secondary px-4" data-bs-dismiss="modal">Batal</button>
|
||||
<button type="submit" class="btn btn-primary px-4 shadow-sm">
|
||||
<i class="bi bi-save me-1"></i> Simpan Ulasan
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="text-center p-5 text-muted">
|
||||
<i class="bi bi-clipboard-x display-4 d-block mb-3 opacity-25"></i>
|
||||
Tiada borang soal selidik ditemui.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Pagination --}}
|
||||
<div class="d-flex justify-content-center mt-4">
|
||||
{{ $surveys->withQueryString()->links() }}
|
||||
</div>
|
||||
|
||||
<!-- Toast Notification for Copy Success -->
|
||||
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
|
||||
<div id="copyToast" class="toast align-items-center text-white bg-success border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<i class="bi bi-check-circle-fill me-2"></i>Link berjaya disalin!
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function copySurveyLink(surveyUuid) {
|
||||
const baseUrl = "{{ url('/') }}";
|
||||
const surveyUrl = `${baseUrl}/survey/${surveyUuid}`;
|
||||
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(surveyUrl).then(function() {
|
||||
showCopyToast();
|
||||
}).catch(function(err) {
|
||||
console.error('Clipboard API failed: ', err);
|
||||
fallbackCopy(surveyUrl);
|
||||
});
|
||||
} else {
|
||||
fallbackCopy(surveyUrl);
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackCopy(text) {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
showCopyToast();
|
||||
} else {
|
||||
alert('Gagal menyalin link. Sila cuba lagi.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fallback copy failed: ', err);
|
||||
alert('Gagal menyalin link. Sila cuba lagi.');
|
||||
}
|
||||
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
// Show toast notification
|
||||
function showCopyToast() {
|
||||
const toastEl = document.getElementById('copyToast');
|
||||
const toast = new bootstrap.Toast(toastEl);
|
||||
toast.show();
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
198
resources/views/admin/surveys/print_statistics.blade.php
Normal file
198
resources/views/admin/surveys/print_statistics.blade.php
Normal file
@@ -0,0 +1,198 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ms">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Statistik - {{ $survey->title }}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: white !important;
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.break-inside-avoid {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 15px !important;
|
||||
}
|
||||
.chart-container {
|
||||
height: 220px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.card-body {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
.card-header {
|
||||
padding: 0.5rem 1rem !important;
|
||||
}
|
||||
h1.h3 { font-size: 1.5rem !important; }
|
||||
h5 { font-size: 1rem !important; }
|
||||
.table-sm { font-size: 0.8rem !important; }
|
||||
|
||||
@media print {
|
||||
.no-print { display: none !important; }
|
||||
.card { border: 1px solid #ddd !important; box-shadow: none !important; }
|
||||
.badge { border: 1px solid #ccc !important; color: black !important; background: white !important; }
|
||||
body { padding: 0; }
|
||||
.container-fluid { padding: 0; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="text-center mb-4 border-bottom pb-2">
|
||||
<h1 class="fw-bold h3 mb-1">{{ $survey->title }}</h1>
|
||||
<p class="text-muted mb-0 small">Laporan Statistik - {{ date('d/m/Y H:i') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6">
|
||||
<div class="card text-center py-1 bg-light border-0">
|
||||
<div class="card-body py-2">
|
||||
<h4 class="fw-bold mb-0">{{ $totalSurveyRespondents }}</h4>
|
||||
<small class="text-muted">Responden</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="card text-center py-1 bg-light border-0">
|
||||
<div class="card-body py-2">
|
||||
<h4 class="fw-bold mb-0">{{ $totalSurveyQuestions }}</h4>
|
||||
<small class="text-muted">Soalan</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@foreach($results as $questionId => $result)
|
||||
<div class="card mb-3 break-inside-avoid border shadow-none">
|
||||
<div class="card-header bg-light border-bottom p-2">
|
||||
<h6 class="fw-bold mb-0">Soalan {{ $loop->iteration }}: {{ $result['question'] }}</h6>
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
@if(isset($result['type']) && $result['type'] == 'text')
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped table-sm mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 40px;">#</th>
|
||||
<th>Jawapan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($result['data'] as $answer)
|
||||
<tr>
|
||||
<td>{{ $loop->iteration }}</td>
|
||||
<td>{{ $answer }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">Tiada jawapan.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-7">
|
||||
<div class="chart-container">
|
||||
<canvas id="chart-{{ $questionId }}"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<table class="table table-bordered table-sm mb-0" style="font-size: 0.75rem;">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Pilihan Jawapan</th>
|
||||
<th class="text-center">Kekerapan</th>
|
||||
<th class="text-center">Peratus(%)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($result['data'] as $stat)
|
||||
<tr>
|
||||
<td>{{ $stat['label'] }}</td>
|
||||
<td class="text-center">{{ $stat['count'] }}</td>
|
||||
<td class="text-center fw-bold">{{ $stat['percentage'] }}%</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
<tr class="table-secondary fw-bold">
|
||||
<td>JUMLAH</td>
|
||||
<td class="text-center">{{ $result['total'] }}</td>
|
||||
<td class="text-center">100%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(!empty($result['other_answers']))
|
||||
<div class="mt-2 border-top pt-1">
|
||||
<small class="fw-bold text-secondary" style="font-size: 0.7rem;">Lain-lain:</small>
|
||||
<div class="d-flex flex-wrap gap-1 mt-1">
|
||||
@foreach($result['other_answers'] as $otherAns)
|
||||
<span class="badge bg-white text-dark border p-1 fw-normal" style="font-size: 0.7rem;">{{ $otherAns }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
<div class="text-center mt-3 no-print">
|
||||
<button onclick="window.print()" class="btn btn-primary px-4 py-1 fw-bold">KLIK UNTUK CETAK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const results = @json($results);
|
||||
|
||||
Object.keys(results).forEach(id => {
|
||||
const data = results[id];
|
||||
if(data.type === 'text') return;
|
||||
|
||||
const ctx = document.getElementById(`chart-${id}`).getContext('2d');
|
||||
const labels = data.data.map(item => item.label);
|
||||
const counts = data.data.map(item => item.count);
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Bilangan',
|
||||
data: counts,
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.5)',
|
||||
borderColor: 'rgb(54, 162, 235)',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: { duration: 0 },
|
||||
scales: {
|
||||
y: { beginAtZero: true, ticks: { precision: 0, font: { size: 10 } } },
|
||||
x: { ticks: { font: { size: 10 } } }
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false }
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
window.print();
|
||||
}, 800);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
189
resources/views/admin/surveys/statistics.blade.php
Normal file
189
resources/views/admin/surveys/statistics.blade.php
Normal file
@@ -0,0 +1,189 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@push('styles')
|
||||
<link rel="stylesheet" href="{{ asset('css/adminHeader.css') }}">
|
||||
<link rel="stylesheet" href="{{ asset('css/statisticsAdmin.css') }}">
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="admin-header-box">
|
||||
<div>
|
||||
<h4>Statistik: {{ $survey->title }}</h4>
|
||||
<p>Analisis data respons bagi borang ini.</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ route('admin.surveys.statistics_print', $survey->id) }}" target="_blank" class="btn btn-sm btn-light px-3 fw-bold text-primary">
|
||||
<i class="bi bi-printer me-1"></i> Cetak
|
||||
</a>
|
||||
<a href="{{ route('admin.surveys.index') }}" class="btn btn-sm btn-light px-3 fw-bold text-primary">
|
||||
<i class="bi bi-arrow-left me-1"></i> Kembali
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm border text-center py-2">
|
||||
<div class="card-body p-2">
|
||||
<h3 class="stat-card-number">{{ $totalSurveyRespondents }}</h3>
|
||||
<small class="text-muted">Jumlah Responden</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm border text-center py-2">
|
||||
<div class="card-body p-2">
|
||||
<h3 class="stat-card-number">{{ $totalSurveyQuestions }}</h3>
|
||||
<small class="text-muted">Jumlah Soalan</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@foreach($results as $questionId => $result)
|
||||
<div class="card shadow-sm mb-5 break-inside-avoid">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="fw-bold mb-0">Soalan: {{ $result['question'] }}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if(isset($result['type']) && $result['type'] == 'text')
|
||||
{{-- DISPLAY TEXT ANSWERS --}}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 50px;">#</th>
|
||||
<th>Jawapan Responden</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($result['data'] as $index => $answer)
|
||||
<tr>
|
||||
<td>{{ $loop->iteration }}</td>
|
||||
<td>{{ $answer }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">Tiada jawapan.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
{{-- DISPLAY CHART FOR RADIO/CHECKBOX --}}
|
||||
<div class="row">
|
||||
<div class="col-md-7 mb-4 mb-md-0">
|
||||
<div class="chart-container-box">
|
||||
<canvas id="chart-{{ $questionId }}"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<table class="table table-bordered table-striped table-hover">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th class="text-center">Pilihan Jawapan</th>
|
||||
<th class="text-center">Kekerapan</th>
|
||||
<th class="text-center">Peratus (%)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($result['data'] as $stat)
|
||||
<tr>
|
||||
<td>{{ $stat['label'] }}</td>
|
||||
<td class="text-center">{{ $stat['count'] }}</td>
|
||||
<td class="text-center fw-bold">{{ $stat['percentage'] }}%</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
<tr class="table-secondary fw-bold">
|
||||
<td>JUMLAH</td>
|
||||
<td class="text-center">{{ $result['total'] }}</td>
|
||||
<td class="text-center">100%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(!empty($result['other_answers']))
|
||||
<div class="mt-4">
|
||||
<h6 class="fw-bold text-secondary">Jawapan Lain-lain:</h6>
|
||||
<ul class="list-group list-group-flush border rounded">
|
||||
@foreach($result['other_answers'] as $otherAns)
|
||||
<li class="list-group-item other-answer-item">{{ $otherAns }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- Load Chart.js CDN (Kalau belum ada dalam layout) --}}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
const results = @json($results);
|
||||
|
||||
Object.keys(results).forEach(id => {
|
||||
const data = results[id];
|
||||
|
||||
if(data.type === 'text') return;
|
||||
|
||||
const ctx = document.getElementById(`chart-${id}`).getContext('2d');
|
||||
|
||||
// Extract labels dan values untuk chart
|
||||
const labels = data.data.map(item => item.label);
|
||||
const counts = data.data.map(item => item.count);
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Bilangan Responden',
|
||||
data: counts,
|
||||
backgroundColor: [
|
||||
'rgba(255, 99, 132, 0.2)',
|
||||
'rgba(255, 159, 64, 0.2)',
|
||||
'rgba(255, 205, 86, 0.2)',
|
||||
'rgba(75, 192, 192, 0.2)',
|
||||
'rgba(54, 162, 235, 0.2)',
|
||||
'rgba(153, 102, 255, 0.2)',
|
||||
'rgba(201, 203, 207, 0.2)'],
|
||||
borderColor: [
|
||||
'rgb(255, 99, 132)',
|
||||
'rgb(255, 159, 64)',
|
||||
'rgb(255, 205, 86)',
|
||||
'rgb(75, 192, 192)',
|
||||
'rgb(54, 162, 235)',
|
||||
'rgb(153, 102, 255)',
|
||||
'rgb(201, 203, 207)'],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
// indexAxis: 'y',
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
106
resources/views/admin/surveys/ulasan.blade.php
Normal file
106
resources/views/admin/surveys/ulasan.blade.php
Normal file
@@ -0,0 +1,106 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@push('styles')
|
||||
<link rel="stylesheet" href="{{ asset('css/adminHeader.css') }}">
|
||||
<link rel="stylesheet" href="{{ asset('css/ulasanAdmin.css') }}">
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
{{-- Header --}}
|
||||
<div class="admin-header-box">
|
||||
<div>
|
||||
<h4>Nota Post-Mortem</h4>
|
||||
<p>Klik kad untuk baca ulasan penuh.</p>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<a href="{{ route('admin.surveys.ulasan.export') }}" class="btn btn-sm btn-success rounded-pill px-3 fw-bold me-2">
|
||||
<i class="bi bi-file-earmark-excel me-1"></i> Export
|
||||
</a>
|
||||
|
||||
{{-- Search Form --}}
|
||||
<form method="GET" action="{{ route('admin.surveys.ulasan') }}" class="d-flex gap-2 mb-0">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" name="search" value="{{ request('search') }}" class="form-control rounded-pill px-3" placeholder="Cari tajuk..." style="width: 200px;">
|
||||
<button class="btn btn-light rounded-circle ms-1 p-1 d-flex align-items-center justify-content-center" type="submit" style="width: 30px; height: 30px;">
|
||||
<i class="bi bi-search text-primary"></i>
|
||||
</button>
|
||||
@if(request('search'))
|
||||
<a href="{{ route('admin.surveys.ulasan') }}" class="btn btn-light rounded-circle ms-1 p-1 d-flex align-items-center justify-content-center" style="width: 30px; height: 30px;">
|
||||
<i class="bi bi-x-lg text-danger"></i>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<a href="{{ route('admin.surveys.index') }}" class="btn btn-sm btn-light rounded-pill px-3 fw-bold text-primary">
|
||||
<i class="bi bi-arrow-left me-1"></i> Kembali
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
@forelse($surveys as $survey)
|
||||
<div class="col-6 col-md-4 col-lg-3 mb-2">
|
||||
<div class="card h-100 border-0 shadow-sm rounded-4 position-relative overflow-hidden cursor-pointer ulasan-hover-effect"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#ulasanModal{{ $survey->id }}"
|
||||
style="cursor: pointer; background: #ffffff;">
|
||||
|
||||
<div class="position-absolute top-0 start-0 h-100 bg-primary-subtle" style="width: 3px;"></div>
|
||||
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<small class="text-primary fw-bold" style="font-size: 0.65rem;">#{{ $survey->id }}</small>
|
||||
<small class="text-muted" style="font-size: 0.65rem;">{{ $survey->updated_at->format('d/m/y') }}</small>
|
||||
</div>
|
||||
|
||||
<h6 class="fw-bold text-dark mb-2 text-truncate" style="text-transform: uppercase; font-size: 0.9rem;">
|
||||
{{ $survey->title }}
|
||||
</h6>
|
||||
|
||||
<div class="p-2 rounded-3" style="background-color: #f8f9fa;">
|
||||
<p class="text-secondary mb-0" style="font-size: 0.75rem; line-height: 1.4;">
|
||||
{{ Str::limit($survey->ulasan, 60) }}
|
||||
@if(strlen($survey->ulasan) > 60)
|
||||
<span class="text-primary fw-bold" style="font-size: 0.7rem;">...more</span>
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- MODAL UNTUK AYAT PENUH --}}
|
||||
<div class="modal fade" id="ulasanModal{{ $survey->id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-0 rounded-4 shadow">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<h6 class="modal-title fw-bold text-primary">Ulasan Penuh</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body pt-2">
|
||||
<p class="text-muted small mb-1">Tajuk Borang:</p>
|
||||
<h6 class="fw-bold mb-3">{{ $survey->title }}</h6>
|
||||
<hr class="opacity-10">
|
||||
<p class="text-dark" style="white-space: pre-line; line-height: 1.6; font-size: 0.9rem;">
|
||||
{{ $survey->ulasan }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer border-0 pt-0">
|
||||
<small class="text-muted me-auto" style="font-size: 0.7rem;">Oleh: {{ $survey->user->name ?? 'Admin' }}</small>
|
||||
<button type="button" class="btn btn-sm btn-secondary rounded-pill px-3" data-bs-dismiss="modal">Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@empty
|
||||
<div class="col-12 text-center py-5">
|
||||
<p class="text-muted small">Tiada ulasan ditemui.</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
182
resources/views/admin/users/index.blade.php
Normal file
182
resources/views/admin/users/index.blade.php
Normal file
@@ -0,0 +1,182 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@push('styles')
|
||||
<link rel="stylesheet" href="{{ asset('css/adminHeader.css') }}">
|
||||
<link rel="stylesheet" href="{{ asset('css/usersAdmin.css') }}">
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
{{-- Header --}}
|
||||
<div class="admin-header-box">
|
||||
<div>
|
||||
<h4>Pengurusan Pengguna</h4>
|
||||
<p>Senarai pengguna dalam sistem berserta nombor pekerja, jabatan, dan role.</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-light px-3 fw-bold text-primary" data-bs-toggle="modal" data-bs-target="#addUserModal">
|
||||
<i class="bi bi-plus-lg"></i> Tambah Pengguna
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-check-circle me-2"></i>{{ session('success') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Search Form --}}
|
||||
<form method="GET" action="{{ route('admin.users.index') }}" class="mb-4">
|
||||
<div class="input-group shadow-sm">
|
||||
<input type="text" name="search" value="{{ request('search') }}" class="form-control border-end-0" placeholder="Cari nama, nombor pekerja atau jabatan...">
|
||||
<button class="btn btn-primary" type="submit"><i class="bi bi-search"></i></button>
|
||||
<a href="{{ route('admin.users.index') }}" class="btn btn-outline-secondary"><i class="bi bi-arrow-counterclockwise"></i></a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="card user-table-card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="px-4">ID</th>
|
||||
<th>Nama</th>
|
||||
<th>Nombor Pekerja</th>
|
||||
<th>Jabatan</th>
|
||||
<th>Role</th>
|
||||
<th class="text-center">Tindakan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($users as $user)
|
||||
<tr>
|
||||
<td class="px-4">{{ $user->id }}</td>
|
||||
<td>{{ $user->name }}</td>
|
||||
<td>{{ $user->no_pekerja }}</td>
|
||||
<td>{{ $user->jabatan ?? 'N/A' }}</td>
|
||||
<td>
|
||||
<span class="badge {{ $user->role == 'admin' ? 'badge-admin' : 'badge-staff' }}">
|
||||
{{ ucfirst($user->role) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button class="btn btn-sm btn-warning me-1" data-bs-toggle="modal" data-bs-target="#editUser{{ $user->id }}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<form action="{{ route('admin.users.delete', $user->id) }}" method="POST" class="d-inline">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button class="btn btn-sm btn-danger" onclick="return confirm('Hapus pengguna ini?')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{{-- Edit Modal --}}
|
||||
<div class="modal fade" id="editUser{{ $user->id }}" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="{{ route('admin.users.update', $user->id) }}">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Edit Pengguna</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nama</label>
|
||||
<input type="text" name="name" class="form-control" value="{{ $user->name }}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nombor Pekerja</label>
|
||||
<input type="text" name="no_pekerja" class="form-control" value="{{ $user->no_pekerja }}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Jabatan</label>
|
||||
<input type="text" name="jabatan" class="form-control" value="{{ $user->jabatan }}" placeholder="Contoh: IT, HR, Kewangan" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password</label>
|
||||
<input type="password" name="password" class="form-control" placeholder="Kosongkan jika tidak mahu tukar">
|
||||
<small class="text-muted">Biarkan kosong untuk kekalkan password semasa</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Role</label>
|
||||
<select name="role" class="form-select" required>
|
||||
<option value="admin" {{ $user->role == 'admin' ? 'selected' : '' }}>Admin</option>
|
||||
<option value="staff" {{ $user->role == 'staff' ? 'selected' : '' }}>Staff</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Tutup</button>
|
||||
<button type="submit" class="btn btn-primary">Simpan Perubahan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-5">
|
||||
<i class="bi bi-people text-muted" style="font-size: 3rem;"></i>
|
||||
<p class="text-muted mt-3 mb-0">Tiada pengguna dijumpai</p>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Add User Modal --}}
|
||||
<div class="modal fade" id="addUserModal" tabindex="-1" aria-labelledby="addUserModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="{{ route('admin.users.store') }}">
|
||||
@csrf
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="addUserModalLabel">Tambah Pengguna Baharu</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Nama</label>
|
||||
<input type="text" id="name" name="name" class="form-control" placeholder="Nama penuh" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="no_pekerja" class="form-label">Nombor Pekerja</label>
|
||||
<input type="text" id="no_pekerja" name="no_pekerja" class="form-control" placeholder="Nombor pekerja" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="jabatan" class="form-label">Jabatan</label>
|
||||
<input type="text" id="jabatan" name="jabatan" class="form-control" placeholder="Contoh: IT, HR, Kewangan" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" id="password" name="password" class="form-control" placeholder="Minimum 6 aksara" required>
|
||||
<small class="text-muted">Minimum 6 aksara</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="role" class="form-label">Role</label>
|
||||
<select id="role" name="role" class="form-select" required>
|
||||
<option value="" disabled selected>Pilih role</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="staff">Staff</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Tutup</button>
|
||||
<button type="submit" class="btn btn-success">Simpan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
62
resources/views/auth/login.blade.php
Normal file
62
resources/views/auth/login.blade.php
Normal file
@@ -0,0 +1,62 @@
|
||||
@extends('layouts.page')
|
||||
|
||||
@section('content')
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
<link rel="stylesheet" href="{{ asset('css/login.css') }}">
|
||||
|
||||
<div class="container">
|
||||
<div class="login-box">
|
||||
|
||||
<div class="form-section">
|
||||
<img src="{{ asset('images/mbip.png') }}" alt="MBIP Logo" class="logo-mbip">
|
||||
|
||||
<h2>Selamat Datang</h2>
|
||||
<p>Sila masukkan maklumat berikut :</p>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success">
|
||||
<i class="bi bi-check-circle-fill"></i>{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
<i class="bi bi-exclamation-circle-fill"></i>{{ $errors->first() }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('login.post') }}">
|
||||
@csrf
|
||||
|
||||
<div class="input-group">
|
||||
<label>Nombor Pekerja</label>
|
||||
<input type="text" name="no_pekerja" placeholder="••••••••"
|
||||
value="{{ old('no_pekerja') }}" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>Kata Laluan</label>
|
||||
<input type="password" name="password" placeholder="••••••••" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-signin">Log Masuk</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="content-section">
|
||||
<h1 class="content-title">myEventPostMortem</h1>
|
||||
<div class="p-4 text-white border-bottom border-secondary">
|
||||
<h6 class="mb-0 fw-bold" style="font-size:1.25rem;">Untuk Tujuan Ujicuba. Data akan direset sesuka hati.</h6>
|
||||
</div>
|
||||
<p class="content-desc">Sistem Rujukan Majlis-majlis di bawah MBIP.</p>
|
||||
|
||||
<div class="image-placeholder">
|
||||
<img src="{{ asset('images/mbippp.jpg') }}" alt="Gambar 1" class="img-fluid custom-img">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
160
resources/views/layouts/app.blade.php
Normal file
160
resources/views/layouts/app.blade.php
Normal file
@@ -0,0 +1,160 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>myEventPostmortem</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
|
||||
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
|
||||
|
||||
@stack('styles')
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container-fluid h-100 p-0">
|
||||
<div class="row g-0 h-100 flex-nowrap">
|
||||
|
||||
<!-- Desktop Sidebar -->
|
||||
<div class="col-auto col-md-3 col-lg-2 bg-dark d-none d-md-flex flex-column h-100 sidebar">
|
||||
|
||||
<div class="p-4 text-white border-bottom border-secondary">
|
||||
<h4 class="mb-0 fw-bold" style="font-size:1.25rem;">myEventPostmortem</h4>
|
||||
</div>
|
||||
<div class="p-4 text-white border-bottom border-secondary">
|
||||
<h6 class="mb-0 fw-bold" style="font-size:1.25rem;">Untuk Tujuan Ujicuba. Data akan direset sesuka hati.</h6>
|
||||
</div>
|
||||
|
||||
@auth
|
||||
<ul class="nav nav-pills flex-column mb-auto px-2">
|
||||
{{-- ADMIN Links --}}
|
||||
@if(auth()->user()->role == 'admin')
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.dashboard') }}" class="nav-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
|
||||
<i class="bi bi-speedometer2 me-2"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.surveys.index') }}" class="nav-link {{ request()->routeIs('admin.surveys.index') ? 'active' : '' }}">
|
||||
<i class="bi bi-clipboard-data me-2"></i> Borang soal selidik
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.responses.list') }}" class="nav-link {{ request()->routeIs('admin.responses.list') ? 'active' : '' }}">
|
||||
<i class="bi bi-clipboard-check me-2"></i> Respons
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.surveys.ulasan') }}" class="nav-link {{ request()->routeIs('admin.surveys.ulasan') ? 'active' : '' }}">
|
||||
<i class="bi bi-pencil-square me-2"></i> Ulasan / Keputusan
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.users.index') }}" class="nav-link {{ request()->routeIs('admin.users.index') ? 'active' : '' }}">
|
||||
<i class="bi bi-people me-2"></i> Senarai pengguna
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
|
||||
<div class="mt-auto p-3">
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button class="btn btn-logout" type="submit">
|
||||
<i class="bi bi-box-arrow-right me-4"></i> Log keluar
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
<!-- Offcanvas Sidebar for Mobile -->
|
||||
<div class="offcanvas offcanvas-start bg-dark text-white" tabindex="-1" id="mobileSidebar">
|
||||
<div class="offcanvas-header border-bottom border-secondary">
|
||||
<h5 class="mb-0 fw-bold">myEventPostmortem</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas"></button>
|
||||
</div>
|
||||
<div class="offcanvas-body d-flex flex-column">
|
||||
<ul class="nav nav-pills flex-column mb-auto px-2">
|
||||
{{-- ADMIN Links --}}
|
||||
@auth
|
||||
@if(auth()->user()->role == 'admin')
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.dashboard') }}" class="nav-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
|
||||
<i class="bi bi-speedometer2 me-2"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.surveys.index') }}" class="nav-link {{ request()->routeIs('admin.surveys.index') ? 'active' : '' }}">
|
||||
<i class="bi bi-clipboard-data me-2"></i> Borang soal selidik
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.responses.list') }}" class="nav-link {{ request()->routeIs('admin.responses.list') ? 'active' : '' }}">
|
||||
<i class="bi bi-clipboard-check me-2"></i> Respons
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.surveys.ulasan') }}" class="nav-link {{ request()->routeIs('admin.surveys.ulasan') ? 'active' : '' }}">
|
||||
<i class="bi bi-pencil-square me-2"></i> Ulasan / Keputusan
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.users.index') }}" class="nav-link {{ request()->routeIs('admin.users.index') ? 'active' : '' }}">
|
||||
<i class="bi bi-people me-2"></i> Senarai pengguna
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@endauth
|
||||
|
||||
|
||||
</ul>
|
||||
|
||||
<div class="mt-auto p-3">
|
||||
@auth
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button class="btn btn-logout w-100" type="submit">
|
||||
<i class="bi bi-box-arrow-right me-2"></i> Log keluar
|
||||
</button>
|
||||
</form>
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="col overflow-auto">
|
||||
|
||||
<header class="d-flex justify-content-between align-items-center py-3 px-4 border-bottom bg-white sticky-top shadow-sm">
|
||||
<!-- Hamburger button visible only on small screens -->
|
||||
<button class="btn btn-outline-secondary d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#mobileSidebar">
|
||||
<i class="bi bi-list"></i>
|
||||
</button>
|
||||
|
||||
@auth
|
||||
<h5 class="mb-0 fw-semibold text-dark">
|
||||
{{ ucfirst(auth()->user()->role) }}
|
||||
</h5>
|
||||
<span class="text-secondary fw-semibold">
|
||||
Hai, {{ auth()->user()->name }}
|
||||
</span>
|
||||
@endauth
|
||||
</header>
|
||||
|
||||
<main class="p-3">
|
||||
@yield('content')
|
||||
</main>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
@stack('scripts')
|
||||
|
||||
</body>
|
||||
</html>
|
||||
18
resources/views/layouts/page.blade.php
Normal file
18
resources/views/layouts/page.blade.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>@yield('title', 'myEventPostmortem')</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-light d-flex flex-column min-vh-100">
|
||||
|
||||
<div class="container-fluid flex-grow-1 d-flex justify-content-center align-items-center">
|
||||
@yield('content')
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user