Initial Commit
This commit is contained in:
commit
99e5c1c5bd
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
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=smtp
|
||||||
|
MAIL_SCHEME=smtp
|
||||||
|
MAIL_HOST=smtp.gmail.com
|
||||||
|
MAIL_PORT=587
|
||||||
|
MAIL_USERNAME=emailanda@gmail.com
|
||||||
|
MAIL_PASSWORD=passwordyangdidapatdariapppasswords
|
||||||
|
MAIL_ENCRYPTION=tls
|
||||||
|
MAIL_FROM_ADDRESS=emailanda@gmail.com
|
||||||
|
MAIL_FROM_NAME="SIKOS - Sistem Informasi Kegiatan Organisasi"
|
||||||
|
|
||||||
|
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}"
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
Pada migrasi database terdapat penambahan kolom baru "Keterangan"
|
||||||
|
Pada migrasi database terdapat perubahan dan penambahan isi status pada kolom "Status" (Disetujui Pembina, Disetujui Admin, Ditolak Pembina, Ditolak Admin)
|
||||||
|
Pada migrasi database terdapat penambahan tabel baru bernama "notifikasi"
|
||||||
|
Pada migrasi database terdapat penambahakn kolom photo_profile_path untuk kolom "Users"
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
<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.
|
||||||
|
|
||||||
|
In addition, [Laracasts](https://laracasts.com) 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.
|
||||||
|
|
||||||
|
You can also watch bite-sized lessons with real-world projects on [Laravel Learn](https://laravel.com/learn), where you will be guided through building a Laravel application from scratch while learning PHP fundamentals.
|
||||||
|
|
||||||
|
## Agentic Development
|
||||||
|
|
||||||
|
Laravel's predictable structure and conventions make it ideal for AI coding agents like Claude Code, Cursor, and GitHub Copilot. Install [Laravel Boost](https://laravel.com/docs/ai) to supercharge your AI workflow:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require laravel/boost --dev
|
||||||
|
|
||||||
|
php artisan boost:install
|
||||||
|
```
|
||||||
|
|
||||||
|
Boost provides your agent 15+ tools and skills that help agents build Laravel applications while following best practices.
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\AnggotaOrganisasi;
|
||||||
|
use App\Models\Organisasi;
|
||||||
|
use App\Models\PertemuanEkskul;
|
||||||
|
use Barryvdh\DomPDF\Facade\Pdf;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class AbsensiEkskulController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Daftar semua organisasi dengan ringkasan pertemuan.
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request): View
|
||||||
|
{
|
||||||
|
$search = trim((string) $request->query('search', ''));
|
||||||
|
|
||||||
|
$query = Organisasi::withCount('pertemuanEkskul', 'anggota');
|
||||||
|
|
||||||
|
if ($search !== '') {
|
||||||
|
$query->where('nama_organisasi', 'like', "%{$search}%");
|
||||||
|
}
|
||||||
|
|
||||||
|
$organisasiList = $query->orderBy('nama_organisasi')->get();
|
||||||
|
|
||||||
|
return view('admin.absensi-ekskul.index', compact('organisasiList', 'search'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Riwayat pertemuan dan rekap kehadiran per organisasi.
|
||||||
|
*/
|
||||||
|
public function show(Request $request, Organisasi $organisasi): View
|
||||||
|
{
|
||||||
|
$semester = $request->query('semester');
|
||||||
|
$tahunAjaran = $request->query('tahun_ajaran');
|
||||||
|
|
||||||
|
// Daftar pertemuan
|
||||||
|
$pertemuanQuery = PertemuanEkskul::where('organisasi_id', $organisasi->id)
|
||||||
|
->withCount([
|
||||||
|
'absensi as hadir_count' => fn ($q) => $q->where('status', 'hadir'),
|
||||||
|
'absensi as izin_count' => fn ($q) => $q->where('status', 'izin'),
|
||||||
|
'absensi as sakit_count' => fn ($q) => $q->where('status', 'sakit'),
|
||||||
|
'absensi as alfa_count' => fn ($q) => $q->where('status', 'alfa'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($semester) {
|
||||||
|
$pertemuanQuery->where('semester', $semester);
|
||||||
|
}
|
||||||
|
if ($tahunAjaran) {
|
||||||
|
$pertemuanQuery->where('tahun_ajaran', $tahunAjaran);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pertemuan = $pertemuanQuery->orderByDesc('tanggal')
|
||||||
|
->orderByDesc('pertemuan_ke')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Rekap kehadiran
|
||||||
|
$pertemuanIds = $pertemuan->pluck('id');
|
||||||
|
$totalPertemuan = $pertemuanIds->count();
|
||||||
|
|
||||||
|
$anggota = AnggotaOrganisasi::where('organisasi_id', $organisasi->id)
|
||||||
|
->withCount([
|
||||||
|
'absensi as hadir_count' => fn ($q) => $q->whereIn('pertemuan_id', $pertemuanIds)->where('status', 'hadir'),
|
||||||
|
'absensi as izin_count' => fn ($q) => $q->whereIn('pertemuan_id', $pertemuanIds)->where('status', 'izin'),
|
||||||
|
'absensi as sakit_count' => fn ($q) => $q->whereIn('pertemuan_id', $pertemuanIds)->where('status', 'sakit'),
|
||||||
|
'absensi as alfa_count' => fn ($q) => $q->whereIn('pertemuan_id', $pertemuanIds)->where('status', 'alfa'),
|
||||||
|
])
|
||||||
|
->orderBy('nama')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Generate list tahun ajaran & semester secara dinamis berdasarkan pertemuan yang ada
|
||||||
|
$tahunAjaranList = PertemuanEkskul::where('organisasi_id', $organisasi->id)
|
||||||
|
->distinct()
|
||||||
|
->pluck('tahun_ajaran')
|
||||||
|
->filter()
|
||||||
|
->toArray();
|
||||||
|
sort($tahunAjaranList);
|
||||||
|
|
||||||
|
$semesterList = PertemuanEkskul::where('organisasi_id', $organisasi->id)
|
||||||
|
->distinct()
|
||||||
|
->pluck('semester')
|
||||||
|
->filter()
|
||||||
|
->toArray();
|
||||||
|
sort($semesterList);
|
||||||
|
|
||||||
|
$tahunList = PertemuanEkskul::where('organisasi_id', $organisasi->id)
|
||||||
|
->selectRaw('YEAR(tanggal) as tahun')
|
||||||
|
->distinct()
|
||||||
|
->orderBy('tahun')
|
||||||
|
->pluck('tahun')
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
if (empty($tahunList)) {
|
||||||
|
$tahunList = [(int) date('Y')];
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('admin.absensi-ekskul.show', compact(
|
||||||
|
'organisasi',
|
||||||
|
'pertemuan',
|
||||||
|
'anggota',
|
||||||
|
'totalPertemuan',
|
||||||
|
'semester',
|
||||||
|
'tahunAjaran',
|
||||||
|
'tahunAjaranList',
|
||||||
|
'semesterList',
|
||||||
|
'tahunList'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detail pertemuan spesifik.
|
||||||
|
*/
|
||||||
|
public function detail(Organisasi $organisasi, PertemuanEkskul $pertemuan): View
|
||||||
|
{
|
||||||
|
if ($pertemuan->organisasi_id !== $organisasi->id) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pertemuan->load(['absensi.anggota', 'fotoKegiatan', 'pembina']);
|
||||||
|
|
||||||
|
return view('admin.absensi-ekskul.detail', compact('organisasi', 'pertemuan'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export rekap kehadiran ke PDF.
|
||||||
|
*/
|
||||||
|
public function exportPdf(Request $request, Organisasi $organisasi)
|
||||||
|
{
|
||||||
|
$exportType = $request->query('export_type', 'semester');
|
||||||
|
$semester = $request->query('semester');
|
||||||
|
$tahunAjaran = $request->query('tahun_ajaran');
|
||||||
|
$bulan = $request->query('bulan');
|
||||||
|
$tahun = $request->query('tahun');
|
||||||
|
|
||||||
|
$pertemuanQuery = PertemuanEkskul::where('organisasi_id', $organisasi->id);
|
||||||
|
|
||||||
|
if ($exportType === 'bulan') {
|
||||||
|
if ($bulan) {
|
||||||
|
$pertemuanQuery->whereMonth('tanggal', $bulan);
|
||||||
|
}
|
||||||
|
if ($tahun) {
|
||||||
|
$pertemuanQuery->whereYear('tanggal', $tahun);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($semester) {
|
||||||
|
$pertemuanQuery->where('semester', $semester);
|
||||||
|
}
|
||||||
|
if ($tahunAjaran) {
|
||||||
|
$pertemuanQuery->where('tahun_ajaran', $tahunAjaran);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pertemuanIds = $pertemuanQuery->pluck('id');
|
||||||
|
$totalPertemuan = $pertemuanIds->count();
|
||||||
|
|
||||||
|
if ($totalPertemuan === 0) {
|
||||||
|
return redirect()->route('admin.absensi-ekskul.show', $organisasi)->with('error', 'Tidak ada data pertemuan pada filter tersebut untuk diexport.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$anggota = AnggotaOrganisasi::where('organisasi_id', $organisasi->id)
|
||||||
|
->withCount([
|
||||||
|
'absensi as hadir_count' => fn ($q) => $q->whereIn('pertemuan_id', $pertemuanIds)->where('status', 'hadir'),
|
||||||
|
'absensi as izin_count' => fn ($q) => $q->whereIn('pertemuan_id', $pertemuanIds)->where('status', 'izin'),
|
||||||
|
'absensi as sakit_count' => fn ($q) => $q->whereIn('pertemuan_id', $pertemuanIds)->where('status', 'sakit'),
|
||||||
|
'absensi as alfa_count' => fn ($q) => $q->whereIn('pertemuan_id', $pertemuanIds)->where('status', 'alfa'),
|
||||||
|
])
|
||||||
|
->orderBy('nama')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$namaBulan = null;
|
||||||
|
if ($exportType === 'bulan' && $bulan) {
|
||||||
|
$bulanIndo = [
|
||||||
|
1 => 'Januari', 2 => 'Februari', 3 => 'Maret', 4 => 'April', 5 => 'Mei', 6 => 'Juni',
|
||||||
|
7 => 'Juli', 8 => 'Agustus', 9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Desember'
|
||||||
|
];
|
||||||
|
$namaBulan = $bulanIndo[(int)$bulan] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdf = Pdf::loadView('admin.absensi-ekskul.rekap_pdf', [
|
||||||
|
'organisasi' => $organisasi,
|
||||||
|
'anggota' => $anggota,
|
||||||
|
'totalPertemuan' => $totalPertemuan,
|
||||||
|
'exportType' => $exportType,
|
||||||
|
'semester' => $semester,
|
||||||
|
'tahunAjaran' => $tahunAjaran,
|
||||||
|
'bulan' => $bulan,
|
||||||
|
'namaBulan' => $namaBulan,
|
||||||
|
'tahun' => $tahun,
|
||||||
|
])->setPaper('a4', 'landscape');
|
||||||
|
|
||||||
|
$filename = 'rekap-absensi-' . \Illuminate\Support\Str::slug($organisasi->nama_organisasi) . '-' . now()->format('YmdHis') . '.pdf';
|
||||||
|
|
||||||
|
return $pdf->download($filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export absensi pertemuan spesifik ke PDF.
|
||||||
|
*/
|
||||||
|
public function exportPertemuanPdf(Organisasi $organisasi, PertemuanEkskul $pertemuan)
|
||||||
|
{
|
||||||
|
if ($pertemuan->organisasi_id !== $organisasi->id) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pertemuan->load(['absensi.anggota', 'pembina']);
|
||||||
|
|
||||||
|
$pdf = Pdf::loadView('pembina.absensi-ekskul.pertemuan_pdf', [
|
||||||
|
'organisasi' => $organisasi,
|
||||||
|
'pertemuan' => $pertemuan,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$filename = 'absensi-pertemuan-' . $pertemuan->pertemuan_ke . '-' . \Illuminate\Support\Str::slug($organisasi->nama_organisasi) . '-' . $pertemuan->tanggal->format('Ymd') . '.pdf';
|
||||||
|
|
||||||
|
return $pdf->download($filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Dokumentasi;
|
||||||
|
use App\Models\Kegiatan;
|
||||||
|
use App\Models\LaporanKegiatan;
|
||||||
|
use App\Models\Organisasi;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class DashboardController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(): View
|
||||||
|
{
|
||||||
|
$stats = [
|
||||||
|
'total_pengguna' => User::count(),
|
||||||
|
'total_organisasi' => Organisasi::count(),
|
||||||
|
'total_kegiatan' => Kegiatan::count(),
|
||||||
|
'kegiatan_disetujui' => Kegiatan::whereIn('status', ['disetujui admin', 'disetujui pembina'])->count(),
|
||||||
|
'kegiatan_ditolak' => Kegiatan::whereIn('status', ['ditolak admin', 'ditolak pembina'])->count(),
|
||||||
|
'kegiatan_pending' => Kegiatan::where('status', 'pending')->count(),
|
||||||
|
'kegiatan_butuh_validasi' => Kegiatan::where('status', 'disetujui pembina')->count(),
|
||||||
|
'total_dokumentasi' => Dokumentasi::count(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$kegiatanTerbaru = Kegiatan::with('organisasi:id,nama_organisasi')
|
||||||
|
->where('status', 'disetujui admin')
|
||||||
|
->latest('created_at')
|
||||||
|
->take(10)
|
||||||
|
->get(['id', 'organisasi_id', 'nama_kegiatan', 'status', 'tanggal_mulai', 'created_at']);
|
||||||
|
|
||||||
|
$pengajuanTerbaru = Kegiatan::with('organisasi:id,nama_organisasi')
|
||||||
|
->where('status', 'disetujui pembina')
|
||||||
|
->latest('created_at')
|
||||||
|
->take(10)
|
||||||
|
->get(['id', 'organisasi_id', 'nama_kegiatan', 'status', 'tanggal_mulai', 'created_at']);
|
||||||
|
|
||||||
|
// Data chart: organisasi paling aktif berdasarkan jumlah laporan kegiatan
|
||||||
|
$laporanPerOrganisasi = LaporanKegiatan::join('kegiatan', 'laporan_kegiatan.kegiatan_id', '=', 'kegiatan.id')
|
||||||
|
->join('organisasi', 'kegiatan.organisasi_id', '=', 'organisasi.id')
|
||||||
|
->selectRaw('organisasi.id as organisasi_id, organisasi.nama_organisasi, COUNT(laporan_kegiatan.id) as jumlah')
|
||||||
|
->whereNotNull('kegiatan.organisasi_id')
|
||||||
|
->groupBy('organisasi.id', 'organisasi.nama_organisasi')
|
||||||
|
->orderByDesc('jumlah')
|
||||||
|
->take(10)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$chartLabels = $laporanPerOrganisasi
|
||||||
|
->map(fn ($item) => $item->nama_organisasi ?? 'Tidak Diketahui')
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$chartData = $laporanPerOrganisasi
|
||||||
|
->map(fn ($item) => (int) $item->jumlah)
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
return view('admin.dashboard', compact('stats', 'kegiatanTerbaru', 'pengajuanTerbaru', 'chartLabels', 'chartData'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Dokumentasi;
|
||||||
|
use App\Models\Kegiatan;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class DokumentasiController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request): View
|
||||||
|
{
|
||||||
|
$filterNamaKegiatan = trim((string) $request->query('nama_kegiatan', ''));
|
||||||
|
$search = trim((string) $request->query('search', ''));
|
||||||
|
|
||||||
|
$query = Dokumentasi::with([
|
||||||
|
'kegiatan:id,nama_kegiatan,organisasi_id'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// filter nama kegiatan
|
||||||
|
if ($filterNamaKegiatan !== '') {
|
||||||
|
|
||||||
|
$query->whereHas('kegiatan', function ($q) use ($filterNamaKegiatan) {
|
||||||
|
|
||||||
|
$q->where('nama_kegiatan', 'like', '%' . $filterNamaKegiatan . '%');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// search
|
||||||
|
if ($search !== '') {
|
||||||
|
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
|
||||||
|
$q->where('keterangan', 'like', "%{$search}%")
|
||||||
|
->orWhereHas('kegiatan', function ($kegiatanQuery) use ($search) {
|
||||||
|
|
||||||
|
$kegiatanQuery->where('nama_kegiatan', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$dokumentasi = $query->latest('id')->get([
|
||||||
|
'id',
|
||||||
|
'kegiatan_id',
|
||||||
|
'file_dokumentasi',
|
||||||
|
'keterangan',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$kegiatanList = Kegiatan::orderBy('nama_kegiatan')
|
||||||
|
->get([
|
||||||
|
'id',
|
||||||
|
'nama_kegiatan'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view('admin.dokumentasi', compact(
|
||||||
|
'dokumentasi',
|
||||||
|
'kegiatanList',
|
||||||
|
'filterNamaKegiatan',
|
||||||
|
'search'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$files = $request->file('file_dokumentasi', []);
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'kegiatan_id' => [
|
||||||
|
'required',
|
||||||
|
'integer',
|
||||||
|
Rule::exists('kegiatan', 'id')
|
||||||
|
],
|
||||||
|
|
||||||
|
'keterangan' => [
|
||||||
|
'nullable',
|
||||||
|
'array'
|
||||||
|
],
|
||||||
|
|
||||||
|
'keterangan.*' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
'max:255'
|
||||||
|
],
|
||||||
|
|
||||||
|
'file_dokumentasi' => [
|
||||||
|
'required',
|
||||||
|
'array',
|
||||||
|
'min:1'
|
||||||
|
],
|
||||||
|
|
||||||
|
'file_dokumentasi.*' => [
|
||||||
|
'required',
|
||||||
|
'image',
|
||||||
|
'mimes:jpg,jpeg,png,webp',
|
||||||
|
'max:5120'
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$kegiatan = Kegiatan::with('organisasi')->find($validated['kegiatan_id']);
|
||||||
|
|
||||||
|
$namaOrganisasi = $kegiatan?->organisasi?->nama_organisasi ?? 'umum';
|
||||||
|
|
||||||
|
// folder otomatis
|
||||||
|
$folder = 'dokumentasi/' . Str::slug($namaOrganisasi);
|
||||||
|
|
||||||
|
foreach ($files as $index => $file) {
|
||||||
|
|
||||||
|
$filename = time() . '_' . $index . '_' . $file->getClientOriginalName();
|
||||||
|
|
||||||
|
$storedPath = $file->storeAs(
|
||||||
|
$folder,
|
||||||
|
$filename,
|
||||||
|
'public'
|
||||||
|
);
|
||||||
|
|
||||||
|
Dokumentasi::create([
|
||||||
|
'kegiatan_id' => $validated['kegiatan_id'],
|
||||||
|
'keterangan' => $validated['keterangan'][$index] ?? null,
|
||||||
|
'file_dokumentasi' => $storedPath,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.dokumentasi')
|
||||||
|
->with('success', 'Dokumentasi berhasil diunggah.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, Dokumentasi $dokumentasi): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'kegiatan_id' => [
|
||||||
|
'required',
|
||||||
|
'integer',
|
||||||
|
Rule::exists('kegiatan', 'id')
|
||||||
|
],
|
||||||
|
|
||||||
|
'keterangan' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
'max:255'
|
||||||
|
],
|
||||||
|
|
||||||
|
'file_dokumentasi' => [
|
||||||
|
'nullable',
|
||||||
|
'image',
|
||||||
|
'mimes:jpg,jpeg,png,webp',
|
||||||
|
'max:5120'
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// upload file baru
|
||||||
|
if ($request->hasFile('file_dokumentasi')) {
|
||||||
|
|
||||||
|
// hapus file lama
|
||||||
|
if (
|
||||||
|
$dokumentasi->file_dokumentasi &&
|
||||||
|
!str_starts_with($dokumentasi->file_dokumentasi, 'http')
|
||||||
|
) {
|
||||||
|
|
||||||
|
Storage::disk('public')->delete($dokumentasi->file_dokumentasi);
|
||||||
|
}
|
||||||
|
|
||||||
|
$kegiatan = Kegiatan::with('organisasi')
|
||||||
|
->find($validated['kegiatan_id']);
|
||||||
|
|
||||||
|
$namaOrganisasi = $kegiatan?->organisasi?->nama_organisasi ?? 'umum';
|
||||||
|
|
||||||
|
$folder = 'dokumentasi/' . Str::slug($namaOrganisasi);
|
||||||
|
|
||||||
|
$file = $request->file('file_dokumentasi');
|
||||||
|
|
||||||
|
$filename = time() . '_' . $file->getClientOriginalName();
|
||||||
|
|
||||||
|
$validated['file_dokumentasi'] = $file->storeAs(
|
||||||
|
$folder,
|
||||||
|
$filename,
|
||||||
|
'public'
|
||||||
|
);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
unset($validated['file_dokumentasi']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dokumentasi->update($validated);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.dokumentasi')
|
||||||
|
->with('success', 'Dokumentasi berhasil diperbarui.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Dokumentasi $dokumentasi): RedirectResponse
|
||||||
|
{
|
||||||
|
// hapus file
|
||||||
|
if (
|
||||||
|
$dokumentasi->file_dokumentasi &&
|
||||||
|
!str_starts_with($dokumentasi->file_dokumentasi, 'http')
|
||||||
|
) {
|
||||||
|
|
||||||
|
Storage::disk('public')->delete($dokumentasi->file_dokumentasi);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dokumentasi->delete();
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.dokumentasi')
|
||||||
|
->with('success', 'Dokumentasi berhasil dihapus.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,283 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Kegiatan;
|
||||||
|
use App\Models\Organisasi;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class KegiatanController extends Controller
|
||||||
|
{
|
||||||
|
private const ALLOWED_STATUSES = [
|
||||||
|
'pending',
|
||||||
|
'disetujui pembina',
|
||||||
|
'disetujui admin',
|
||||||
|
'ditolak pembina',
|
||||||
|
'ditolak admin',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __invoke(): View
|
||||||
|
{
|
||||||
|
$query = Kegiatan::with([
|
||||||
|
'organisasi:id,nama_organisasi',
|
||||||
|
'penanggungJawab:id,name',
|
||||||
|
])->latest('id');
|
||||||
|
|
||||||
|
$status = request('status');
|
||||||
|
$search = request('search', '');
|
||||||
|
|
||||||
|
// filter status
|
||||||
|
if ($status) {
|
||||||
|
|
||||||
|
if ($status === 'disetujui') {
|
||||||
|
|
||||||
|
$query->whereIn('status', [
|
||||||
|
'disetujui pembina',
|
||||||
|
'disetujui admin'
|
||||||
|
]);
|
||||||
|
|
||||||
|
} elseif ($status === 'ditolak') {
|
||||||
|
|
||||||
|
$query->whereIn('status', [
|
||||||
|
'ditolak pembina',
|
||||||
|
'ditolak admin'
|
||||||
|
]);
|
||||||
|
|
||||||
|
} elseif (in_array($status, self::ALLOWED_STATUSES, true)) {
|
||||||
|
|
||||||
|
$query->where('status', $status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// search
|
||||||
|
if ($search) {
|
||||||
|
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
|
||||||
|
$q->where('nama_kegiatan', 'like', "%{$search}%")
|
||||||
|
->orWhere('deskripsi', 'like', "%{$search}%")
|
||||||
|
->orWhere('tempat', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$kegiatan = $query->get([
|
||||||
|
'id',
|
||||||
|
'organisasi_id',
|
||||||
|
'nama_kegiatan',
|
||||||
|
'deskripsi',
|
||||||
|
'tanggal_mulai',
|
||||||
|
'tempat',
|
||||||
|
'penanggung_jawab',
|
||||||
|
'tanggal_berakhir',
|
||||||
|
'proposal',
|
||||||
|
'status',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$organisasiList = Organisasi::with('pembinaUsers:id,name')->orderBy('nama_organisasi')
|
||||||
|
->get([
|
||||||
|
'id',
|
||||||
|
'nama_organisasi'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view('admin.kegiatan', compact(
|
||||||
|
'kegiatan',
|
||||||
|
'organisasiList',
|
||||||
|
'search',
|
||||||
|
'status'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'organisasi_id' => [
|
||||||
|
'required',
|
||||||
|
'integer',
|
||||||
|
Rule::exists('organisasi', 'id')
|
||||||
|
],
|
||||||
|
|
||||||
|
'nama_kegiatan' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'max:150'
|
||||||
|
],
|
||||||
|
|
||||||
|
'deskripsi' => [
|
||||||
|
'nullable',
|
||||||
|
'string'
|
||||||
|
],
|
||||||
|
|
||||||
|
'penanggung_jawab' => [
|
||||||
|
'nullable',
|
||||||
|
'integer',
|
||||||
|
Rule::exists('users', 'id')
|
||||||
|
],
|
||||||
|
|
||||||
|
'tanggal_mulai' => [
|
||||||
|
'nullable',
|
||||||
|
'date'
|
||||||
|
],
|
||||||
|
|
||||||
|
'tanggal_berakhir' => [
|
||||||
|
'nullable',
|
||||||
|
'date',
|
||||||
|
'after_or_equal:tanggal_mulai'
|
||||||
|
],
|
||||||
|
|
||||||
|
'tempat' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
'max:150'
|
||||||
|
],
|
||||||
|
|
||||||
|
'proposal' => [
|
||||||
|
'required',
|
||||||
|
'file',
|
||||||
|
'mimes:pdf,doc,docx',
|
||||||
|
'max:5120'
|
||||||
|
],
|
||||||
|
|
||||||
|
'status' => [
|
||||||
|
'required',
|
||||||
|
Rule::in(self::ALLOWED_STATUSES)
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// upload proposal
|
||||||
|
if ($request->hasFile('proposal')) {
|
||||||
|
|
||||||
|
$organisasi = Organisasi::find($validated['organisasi_id']);
|
||||||
|
|
||||||
|
$folder = 'proposal/' . Str::slug($organisasi->nama_organisasi);
|
||||||
|
|
||||||
|
$file = $request->file('proposal');
|
||||||
|
|
||||||
|
$filename = time() . '_' . $file->getClientOriginalName();
|
||||||
|
|
||||||
|
$validated['proposal'] = $file->storeAs(
|
||||||
|
$folder,
|
||||||
|
$filename,
|
||||||
|
'public'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Kegiatan::create($validated);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.kegiatan')
|
||||||
|
->with('success', 'Kegiatan berhasil ditambahkan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, Kegiatan $kegiatan): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'organisasi_id' => [
|
||||||
|
'required',
|
||||||
|
'integer',
|
||||||
|
Rule::exists('organisasi', 'id')
|
||||||
|
],
|
||||||
|
|
||||||
|
'nama_kegiatan' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'max:150'
|
||||||
|
],
|
||||||
|
|
||||||
|
'deskripsi' => [
|
||||||
|
'nullable',
|
||||||
|
'string'
|
||||||
|
],
|
||||||
|
|
||||||
|
'penanggung_jawab' => [
|
||||||
|
'nullable',
|
||||||
|
'integer',
|
||||||
|
Rule::exists('users', 'id')
|
||||||
|
],
|
||||||
|
|
||||||
|
'tanggal_mulai' => [
|
||||||
|
'nullable',
|
||||||
|
'date'
|
||||||
|
],
|
||||||
|
|
||||||
|
'tanggal_berakhir' => [
|
||||||
|
'nullable',
|
||||||
|
'date',
|
||||||
|
'after_or_equal:tanggal_mulai'
|
||||||
|
],
|
||||||
|
|
||||||
|
'tempat' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
'max:150'
|
||||||
|
],
|
||||||
|
|
||||||
|
'proposal' => [
|
||||||
|
'nullable',
|
||||||
|
'file',
|
||||||
|
'mimes:pdf,doc,docx',
|
||||||
|
'max:5120'
|
||||||
|
],
|
||||||
|
|
||||||
|
'status' => [
|
||||||
|
'required',
|
||||||
|
Rule::in(self::ALLOWED_STATUSES)
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// upload proposal baru
|
||||||
|
if ($request->hasFile('proposal')) {
|
||||||
|
|
||||||
|
// hapus file lama
|
||||||
|
if ($kegiatan->proposal && !str_starts_with($kegiatan->proposal, 'http')) {
|
||||||
|
|
||||||
|
Storage::disk('public')->delete($kegiatan->proposal);
|
||||||
|
}
|
||||||
|
|
||||||
|
$organisasi = Organisasi::find($validated['organisasi_id']);
|
||||||
|
|
||||||
|
$folder = 'proposal/' . Str::slug($organisasi->nama_organisasi);
|
||||||
|
|
||||||
|
$file = $request->file('proposal');
|
||||||
|
|
||||||
|
$filename = time() . '_' . $file->getClientOriginalName();
|
||||||
|
|
||||||
|
$validated['proposal'] = $file->storeAs(
|
||||||
|
$folder,
|
||||||
|
$filename,
|
||||||
|
'public'
|
||||||
|
);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
unset($validated['proposal']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$kegiatan->update($validated);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.kegiatan')
|
||||||
|
->with('success', 'Data kegiatan berhasil diperbarui.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Kegiatan $kegiatan): RedirectResponse
|
||||||
|
{
|
||||||
|
// hapus proposal
|
||||||
|
if ($kegiatan->proposal && !str_starts_with($kegiatan->proposal, 'http')) {
|
||||||
|
|
||||||
|
Storage::disk('public')->delete($kegiatan->proposal);
|
||||||
|
}
|
||||||
|
|
||||||
|
$kegiatan->delete();
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.kegiatan')
|
||||||
|
->with('success', 'Kegiatan berhasil dihapus.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,203 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\LaporanKegiatan;
|
||||||
|
use App\Models\Kegiatan;
|
||||||
|
use Barryvdh\DomPDF\Facade\Pdf;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
|
class LaporanController extends Controller
|
||||||
|
{
|
||||||
|
private const REPORT_DIRECTORY = 'laporan-kegiatan';
|
||||||
|
|
||||||
|
public function __invoke(Request $request): View
|
||||||
|
{
|
||||||
|
$search = trim((string) $request->query('search', ''));
|
||||||
|
|
||||||
|
$query = $this->baseQuery();
|
||||||
|
|
||||||
|
if ($search !== '') {
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('isi_laporan', 'like', "%{$search}%")
|
||||||
|
->orWhereHas('kegiatan', function ($kegiatanQuery) use ($search) {
|
||||||
|
$kegiatanQuery->where('nama_kegiatan', 'like', "%{$search}%");
|
||||||
|
})
|
||||||
|
->orWhereHas('kegiatan.organisasi', function ($organisasiQuery) use ($search) {
|
||||||
|
$organisasiQuery->where('nama_organisasi', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$laporan = $query->get();
|
||||||
|
|
||||||
|
// Get all activities
|
||||||
|
$kegiatanTersedia = Kegiatan::orderByDesc('tanggal_mulai')
|
||||||
|
->get(['id', 'nama_kegiatan', 'tanggal_mulai']);
|
||||||
|
|
||||||
|
return view('admin.laporan', compact('laporan', 'search', 'kegiatanTersedia'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'kegiatan_id' => [
|
||||||
|
'required',
|
||||||
|
'integer',
|
||||||
|
'exists:kegiatan,id',
|
||||||
|
Rule::unique('laporan_kegiatan', 'kegiatan_id')
|
||||||
|
],
|
||||||
|
'isi_laporan' => [
|
||||||
|
'required',
|
||||||
|
'string'
|
||||||
|
],
|
||||||
|
'file_laporan' => [
|
||||||
|
'required',
|
||||||
|
'file',
|
||||||
|
'mimes:pdf,doc,docx',
|
||||||
|
'max:5120'
|
||||||
|
],
|
||||||
|
], [
|
||||||
|
'kegiatan_id.unique' => 'Kegiatan ini sudah memiliki laporan.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$kegiatan = Kegiatan::findOrFail($validated['kegiatan_id']);
|
||||||
|
$organisasi = $kegiatan->organisasi;
|
||||||
|
|
||||||
|
$folder = self::REPORT_DIRECTORY . '/' . Str::slug($organisasi->nama_organisasi);
|
||||||
|
$file = $request->file('file_laporan');
|
||||||
|
$filename = time() . '_' . $file->getClientOriginalName();
|
||||||
|
|
||||||
|
$validated['file_laporan'] = $file->storeAs(
|
||||||
|
$folder,
|
||||||
|
$filename,
|
||||||
|
'public'
|
||||||
|
);
|
||||||
|
|
||||||
|
LaporanKegiatan::create([
|
||||||
|
'kegiatan_id' => $validated['kegiatan_id'],
|
||||||
|
'isi_laporan' => $validated['isi_laporan'],
|
||||||
|
'file_laporan' => $validated['file_laporan'],
|
||||||
|
'status' => 'disetujui pembina',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.laporan')
|
||||||
|
->with('success', 'Laporan kegiatan berhasil ditambahkan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, LaporanKegiatan $laporan): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'isi_laporan' => [
|
||||||
|
'required',
|
||||||
|
'string'
|
||||||
|
],
|
||||||
|
'file_laporan' => [
|
||||||
|
'nullable',
|
||||||
|
'file',
|
||||||
|
'mimes:pdf,doc,docx',
|
||||||
|
'max:5120'
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->hasFile('file_laporan')) {
|
||||||
|
if ($laporan->file_laporan && !str_starts_with($laporan->file_laporan, 'http')) {
|
||||||
|
Storage::disk('public')->delete($laporan->file_laporan);
|
||||||
|
}
|
||||||
|
|
||||||
|
$kegiatan = $laporan->kegiatan;
|
||||||
|
$organisasi = $kegiatan->organisasi;
|
||||||
|
|
||||||
|
$folder = self::REPORT_DIRECTORY . '/' . Str::slug($organisasi->nama_organisasi);
|
||||||
|
$file = $request->file('file_laporan');
|
||||||
|
$filename = time() . '_' . $file->getClientOriginalName();
|
||||||
|
|
||||||
|
$validated['file_laporan'] = $file->storeAs(
|
||||||
|
$folder,
|
||||||
|
$filename,
|
||||||
|
'public'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated['status'] = 'disetujui pembina';
|
||||||
|
$validated['keterangan'] = null;
|
||||||
|
|
||||||
|
$laporan->update($validated);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.laporan')
|
||||||
|
->with('success', 'Laporan kegiatan berhasil diperbarui.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(LaporanKegiatan $laporan): RedirectResponse
|
||||||
|
{
|
||||||
|
if ($laporan->file_laporan && !str_starts_with($laporan->file_laporan, 'http')) {
|
||||||
|
Storage::disk('public')->delete($laporan->file_laporan);
|
||||||
|
}
|
||||||
|
|
||||||
|
$laporan->delete();
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.laporan')
|
||||||
|
->with('success', 'Laporan kegiatan berhasil dihapus.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exportPdf()
|
||||||
|
{
|
||||||
|
$laporan = $this->baseQuery()->get();
|
||||||
|
|
||||||
|
$pdf = Pdf::loadView('admin.laporan_pdf', [
|
||||||
|
'laporan' => $laporan,
|
||||||
|
])->setPaper('a4', 'landscape');
|
||||||
|
|
||||||
|
return $pdf->download(
|
||||||
|
'laporan-kegiatan-' . now()->format('YmdHis') . '.pdf'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function download(LaporanKegiatan $laporan): StreamedResponse|RedirectResponse
|
||||||
|
{
|
||||||
|
$file = $laporan->file_laporan;
|
||||||
|
|
||||||
|
if (! $file) {
|
||||||
|
return back()->with('error', 'File laporan tidak tersedia.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($file, 'http')) {
|
||||||
|
return redirect()->away($file);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Storage::disk('public')->exists($file)) {
|
||||||
|
return back()->with('error', 'File laporan tidak ditemukan di storage.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Storage::disk('public')->download($file);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function baseQuery(): Builder
|
||||||
|
{
|
||||||
|
return LaporanKegiatan::with([
|
||||||
|
'kegiatan:id,organisasi_id,nama_kegiatan,tanggal_mulai',
|
||||||
|
'kegiatan.organisasi:id,nama_organisasi',
|
||||||
|
])
|
||||||
|
->where('status', 'disetujui pembina')
|
||||||
|
->latest('id')
|
||||||
|
->select([
|
||||||
|
'id',
|
||||||
|
'kegiatan_id',
|
||||||
|
'isi_laporan',
|
||||||
|
'file_laporan',
|
||||||
|
'status',
|
||||||
|
'keterangan',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Organisasi;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class OrganisasiController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request): View
|
||||||
|
{
|
||||||
|
$search = $request->query('search', '');
|
||||||
|
|
||||||
|
$query = Organisasi::with([
|
||||||
|
'pembinaUsers:id,name,profile_photo_path',
|
||||||
|
'ketuaUsers:id,name,profile_photo_path',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('nama_organisasi', 'like', "%{$search}%")
|
||||||
|
->orWhere('deskripsi', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$organisasi = $query->latest('id')->get(['id', 'nama_organisasi', 'deskripsi']);
|
||||||
|
|
||||||
|
$pembinaUsers = User::where('role', 'pembina')
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'name']);
|
||||||
|
|
||||||
|
$ketuaUsers = User::where('role', 'ketua')
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'name']);
|
||||||
|
|
||||||
|
return view('admin.organisasi', compact('organisasi', 'pembinaUsers', 'ketuaUsers', 'search'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'nama_organisasi' => ['required', 'string', 'max:100'],
|
||||||
|
'deskripsi' => ['nullable', 'string'],
|
||||||
|
'pembina_ids' => ['nullable', 'array'],
|
||||||
|
'pembina_ids.*' => ['integer', 'exists:users,id'],
|
||||||
|
'ketua_ids' => ['nullable', 'array'],
|
||||||
|
'ketua_ids.*' => ['integer', 'exists:users,id'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pembinaIds = $validated['pembina_ids'] ?? [];
|
||||||
|
$ketuaIds = $validated['ketua_ids'] ?? [];
|
||||||
|
unset($validated['pembina_ids'], $validated['ketua_ids']);
|
||||||
|
|
||||||
|
$organisasi = Organisasi::create($validated);
|
||||||
|
|
||||||
|
$pembinaPivot = [];
|
||||||
|
foreach ($pembinaIds as $id) {
|
||||||
|
$pembinaPivot[$id] = ['role' => 'pembina'];
|
||||||
|
}
|
||||||
|
$organisasi->pembinaUsers()->sync($pembinaPivot);
|
||||||
|
|
||||||
|
$ketuaPivot = [];
|
||||||
|
foreach ($ketuaIds as $id) {
|
||||||
|
$ketuaPivot[$id] = ['role' => 'ketua'];
|
||||||
|
}
|
||||||
|
$organisasi->ketuaUsers()->sync($ketuaPivot);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.organisasi')
|
||||||
|
->with('success', 'Organisasi berhasil ditambahkan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, Organisasi $organisasi): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'nama_organisasi' => ['required', 'string', 'max:100'],
|
||||||
|
'deskripsi' => ['nullable', 'string'],
|
||||||
|
'pembina_ids' => ['nullable', 'array'],
|
||||||
|
'pembina_ids.*' => ['integer', 'exists:users,id'],
|
||||||
|
'ketua_ids' => ['nullable', 'array'],
|
||||||
|
'ketua_ids.*' => ['integer', 'exists:users,id'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pembinaIds = $validated['pembina_ids'] ?? [];
|
||||||
|
$ketuaIds = $validated['ketua_ids'] ?? [];
|
||||||
|
unset($validated['pembina_ids'], $validated['ketua_ids']);
|
||||||
|
|
||||||
|
$organisasi->update($validated);
|
||||||
|
|
||||||
|
$pembinaPivot = [];
|
||||||
|
foreach ($pembinaIds as $id) {
|
||||||
|
$pembinaPivot[$id] = ['role' => 'pembina'];
|
||||||
|
}
|
||||||
|
$organisasi->pembinaUsers()->sync($pembinaPivot);
|
||||||
|
|
||||||
|
$ketuaPivot = [];
|
||||||
|
foreach ($ketuaIds as $id) {
|
||||||
|
$ketuaPivot[$id] = ['role' => 'ketua'];
|
||||||
|
}
|
||||||
|
$organisasi->ketuaUsers()->sync($ketuaPivot);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.organisasi')
|
||||||
|
->with('success', 'Data organisasi berhasil diperbarui.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Organisasi $organisasi): RedirectResponse
|
||||||
|
{
|
||||||
|
$organisasi->delete();
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.organisasi')
|
||||||
|
->with('success', 'Organisasi berhasil dihapus.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class ProfilController extends Controller
|
||||||
|
{
|
||||||
|
private const PHOTO_DIRECTORY = 'profile-photos';
|
||||||
|
|
||||||
|
public function __invoke(): View
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
return view('admin.profil', compact('user'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => ['required', 'string', 'max:100'],
|
||||||
|
'email' => ['required', 'string', 'email', 'max:100', Rule::unique('users', 'email')->ignore($user->id)],
|
||||||
|
'profile_photo' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120'],
|
||||||
|
'password' => ['nullable', 'string', 'min:8', 'confirmed'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->hasFile('profile_photo')) {
|
||||||
|
if ($user->profile_photo_path) {
|
||||||
|
Storage::disk('public')->delete($user->profile_photo_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $request->file('profile_photo');
|
||||||
|
$filename = 'admin-' . $user->id . '-' . now()->format('YmdHis') . '.' . $file->getClientOriginalExtension();
|
||||||
|
$validated['profile_photo_path'] = $file->storeAs(self::PHOTO_DIRECTORY, $filename, 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($validated['profile_photo']);
|
||||||
|
|
||||||
|
if (!empty($validated['password'])) {
|
||||||
|
$validated['password'] = Hash::make($validated['password']);
|
||||||
|
} else {
|
||||||
|
unset($validated['password']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->update($validated);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.profil')
|
||||||
|
->with('success', 'Profil berhasil diperbarui.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Organisasi;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class UserController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request): View
|
||||||
|
{
|
||||||
|
$search = $request->query('search', '');
|
||||||
|
|
||||||
|
$query = User::with([
|
||||||
|
'organisasiSebagaiKetua:id,nama_organisasi',
|
||||||
|
'organisasiSebagaiPembina:id,nama_organisasi',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('name', 'like', "%{$search}%")
|
||||||
|
->orWhere('email', 'like', "%{$search}%")
|
||||||
|
->orWhere('role', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$users = $query->latest('id')->get(['id', 'name', 'email', 'role', 'profile_photo_path']);
|
||||||
|
|
||||||
|
return view('admin.users', compact('users', 'search'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => ['required', 'string', 'max:100'],
|
||||||
|
'email' => ['required', 'string', 'email', 'max:100', 'unique:users,email'],
|
||||||
|
'role' => ['required', Rule::in(['admin', 'ketua', 'pembina'])],
|
||||||
|
'organisasi_ids' => ['nullable', 'array'],
|
||||||
|
'organisasi_ids.*' => ['integer', Rule::exists('organisasi', 'id')],
|
||||||
|
'password' => ['required', 'string', 'min:8'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$organisasiIds = $validated['organisasi_ids'] ?? [];
|
||||||
|
unset($validated['organisasi_ids']);
|
||||||
|
|
||||||
|
$user = User::create($validated);
|
||||||
|
|
||||||
|
if ($validated['role'] === 'ketua' && !empty($organisasiIds)) {
|
||||||
|
foreach ($organisasiIds as $orgId) {
|
||||||
|
$user->organisasiSebagaiKetua()->attach($orgId, ['role' => 'ketua']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($validated['role'] === 'pembina' && !empty($organisasiIds)) {
|
||||||
|
foreach ($organisasiIds as $orgId) {
|
||||||
|
$user->organisasiSebagaiPembina()->attach($orgId, ['role' => 'pembina']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.users')
|
||||||
|
->with('success', 'User berhasil ditambahkan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, User $user): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => ['required', 'string', 'max:100'],
|
||||||
|
'email' => ['required', 'string', 'email', 'max:100', Rule::unique('users', 'email')->ignore($user->id)],
|
||||||
|
'role' => ['required', Rule::in(['admin', 'ketua', 'pembina'])],
|
||||||
|
'organisasi_ids' => ['nullable', 'array'],
|
||||||
|
'organisasi_ids.*' => ['integer', Rule::exists('organisasi', 'id')],
|
||||||
|
'password' => ['nullable', 'string', 'min:8'],
|
||||||
|
'profile_photo' => ['nullable', 'image', 'mimes:jpeg,png,webp', 'max:5120'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (empty($validated['password'])) {
|
||||||
|
unset($validated['password']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->hasFile('profile_photo')) {
|
||||||
|
if ($user->profile_photo_path) {
|
||||||
|
Storage::disk('public')->delete($user->profile_photo_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $request->file('profile_photo')->store('profile-photos', 'public');
|
||||||
|
$validated['profile_photo_path'] = $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
$organisasiIds = $validated['organisasi_ids'] ?? [];
|
||||||
|
unset($validated['organisasi_ids']);
|
||||||
|
|
||||||
|
$user->update($validated);
|
||||||
|
|
||||||
|
// Detach all existing org relationships first
|
||||||
|
$user->organisasiSebagaiKetua()->detach();
|
||||||
|
$user->organisasiSebagaiPembina()->detach();
|
||||||
|
|
||||||
|
// Re-attach based on role
|
||||||
|
if ($validated['role'] === 'ketua' && !empty($organisasiIds)) {
|
||||||
|
foreach ($organisasiIds as $orgId) {
|
||||||
|
$user->organisasiSebagaiKetua()->attach($orgId, ['role' => 'ketua']);
|
||||||
|
}
|
||||||
|
} elseif ($validated['role'] === 'pembina' && !empty($organisasiIds)) {
|
||||||
|
foreach ($organisasiIds as $orgId) {
|
||||||
|
$user->organisasiSebagaiPembina()->attach($orgId, ['role' => 'pembina']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.users')
|
||||||
|
->with('success', 'Data user berhasil diperbarui.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(User $user): RedirectResponse
|
||||||
|
{
|
||||||
|
if (auth()->id() === $user->id) {
|
||||||
|
return redirect()
|
||||||
|
->route('admin.users')
|
||||||
|
->with('error', 'Akun yang sedang login tidak bisa dihapus.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->delete();
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.users')
|
||||||
|
->with('success', 'User berhasil dihapus.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Kegiatan;
|
||||||
|
use App\Notifications\KegiatanWorkflowNotification;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class ValidasiController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request): View
|
||||||
|
{
|
||||||
|
$search = $request->query('search', '');
|
||||||
|
|
||||||
|
$query = Kegiatan::with('organisasi:id,nama_organisasi')
|
||||||
|
->where('status', 'disetujui pembina');
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('nama_kegiatan', 'like', "%{$search}%")
|
||||||
|
->orWhere('deskripsi', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$kegiatanMenungguValidasiAdmin = $query->latest('id')->get([
|
||||||
|
'id',
|
||||||
|
'organisasi_id',
|
||||||
|
'nama_kegiatan',
|
||||||
|
'deskripsi',
|
||||||
|
'tanggal_mulai',
|
||||||
|
'proposal',
|
||||||
|
'status',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view('admin.validasi', compact('kegiatanMenungguValidasiAdmin', 'search'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function approve(Kegiatan $kegiatan): RedirectResponse
|
||||||
|
{
|
||||||
|
if ($kegiatan->status !== 'disetujui pembina') {
|
||||||
|
return redirect()
|
||||||
|
->route('admin.validasi')
|
||||||
|
->with('error', 'Kegiatan hanya bisa divalidasi admin setelah disetujui pembina.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$kegiatan->update([
|
||||||
|
'status' => 'disetujui admin',
|
||||||
|
'keterangan' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->notifyKetuaSetelahDisetujuiAdmin($kegiatan->fresh());
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.validasi')
|
||||||
|
->with('success', 'Kegiatan "' . $kegiatan->nama_kegiatan . '" telah disetujui admin.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reject(Request $request, Kegiatan $kegiatan): RedirectResponse
|
||||||
|
{
|
||||||
|
if ($kegiatan->status !== 'disetujui pembina') {
|
||||||
|
return redirect()
|
||||||
|
->route('admin.validasi')
|
||||||
|
->with('error', 'Kegiatan hanya bisa divalidasi admin setelah disetujui pembina.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'keterangan' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
'in:Jadwal bentrok dengan kegiatan lain,Melanggar kebijakan sekolah',
|
||||||
|
],
|
||||||
|
'keterangan_custom' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
'max:500',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$keterangan = trim($validated['keterangan_custom'] ?? '') ?: ($validated['keterangan'] ?? null);
|
||||||
|
|
||||||
|
$kegiatan->update([
|
||||||
|
'status' => 'ditolak admin',
|
||||||
|
'keterangan' => $keterangan,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->notifyKetuaDanPembinaSetelahDitolakAdmin($kegiatan->fresh());
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.validasi')
|
||||||
|
->with('success', 'Kegiatan "' . $kegiatan->nama_kegiatan . '" telah ditolak admin.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function notifyKetuaSetelahDisetujuiAdmin(Kegiatan $kegiatan): void
|
||||||
|
{
|
||||||
|
$organisasi = $kegiatan->organisasi()->with('ketuaUsers')->first();
|
||||||
|
$ketuaUsers = $organisasi?->ketuaUsers;
|
||||||
|
|
||||||
|
if (! $ketuaUsers || $ketuaUsers->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($ketuaUsers as $ketua) {
|
||||||
|
$ketua->notify(new KegiatanWorkflowNotification(
|
||||||
|
'Proposal Disetujui Admin',
|
||||||
|
'Proposal kegiatan "' . $kegiatan->nama_kegiatan . '" telah disetujui admin.',
|
||||||
|
route('ketua.kegiatan'),
|
||||||
|
'Lihat kegiatan'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function notifyKetuaDanPembinaSetelahDitolakAdmin(Kegiatan $kegiatan): void
|
||||||
|
{
|
||||||
|
$organisasi = $kegiatan->organisasi()->with(['ketuaUsers', 'pembinaUsers'])->first();
|
||||||
|
$keterangan = $kegiatan->keterangan ?: '-';
|
||||||
|
|
||||||
|
foreach ($organisasi?->ketuaUsers ?? [] as $ketua) {
|
||||||
|
$ketua->notify(new KegiatanWorkflowNotification(
|
||||||
|
'Proposal Ditolak Admin',
|
||||||
|
'Proposal kegiatan "' . $kegiatan->nama_kegiatan . '" ditolak admin. Keterangan: ' . $keterangan,
|
||||||
|
route('ketua.kegiatan'),
|
||||||
|
'Lihat kegiatan'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($organisasi?->pembinaUsers ?? [] as $pembina) {
|
||||||
|
$pembina->notify(new KegiatanWorkflowNotification(
|
||||||
|
'Proposal Binaan Ditolak Admin',
|
||||||
|
'Proposal kegiatan "' . $kegiatan->nama_kegiatan . '" ditolak admin. Keterangan: ' . $keterangan,
|
||||||
|
route('pembina.validasi'),
|
||||||
|
'Lihat validasi'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class AuthController extends Controller
|
||||||
|
{
|
||||||
|
public function showLoginForm(): View
|
||||||
|
{
|
||||||
|
return view('login');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function showRegisterForm(): View
|
||||||
|
{
|
||||||
|
return view('register');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'max:100',
|
||||||
|
'unique:users,name',
|
||||||
|
'different:email',
|
||||||
|
static function (string $attribute, mixed $value, \Closure $fail): void {
|
||||||
|
if (User::where('email', $value)->exists()) {
|
||||||
|
$fail('Username tidak boleh sama dengan email yang sudah terdaftar.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'email' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'email',
|
||||||
|
'max:100',
|
||||||
|
'unique:users,email',
|
||||||
|
'different:name',
|
||||||
|
static function (string $attribute, mixed $value, \Closure $fail): void {
|
||||||
|
if (User::where('name', $value)->exists()) {
|
||||||
|
$fail('Email tidak boleh sama dengan username yang sudah terdaftar.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'password' => ['required', 'string', 'min:8', 'confirmed'],
|
||||||
|
], [
|
||||||
|
'required' => ':attribute wajib diisi.',
|
||||||
|
'email' => ':attribute harus berupa alamat email yang valid.',
|
||||||
|
'max' => ':attribute maksimal :max karakter.',
|
||||||
|
'unique' => ':attribute sudah digunakan.',
|
||||||
|
'different' => ':attribute tidak boleh sama dengan :other.',
|
||||||
|
'min' => ':attribute minimal :min karakter.',
|
||||||
|
'confirmed' => 'Konfirmasi password tidak cocok.',
|
||||||
|
], [
|
||||||
|
'name' => 'Username',
|
||||||
|
'email' => 'Email',
|
||||||
|
'password' => 'Password',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::create([
|
||||||
|
'name' => $validated['name'],
|
||||||
|
'email' => $validated['email'],
|
||||||
|
'password' => $validated['password'],
|
||||||
|
'role' => 'ketua',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Auth::login($user);
|
||||||
|
$request->session()->regenerate();
|
||||||
|
|
||||||
|
return redirect()->route('ketua.dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function login(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'login' => ['required', 'string'],
|
||||||
|
'password' => ['required', 'string'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$loginField = filter_var($validated['login'], FILTER_VALIDATE_EMAIL) ? 'email' : 'name';
|
||||||
|
|
||||||
|
if (! Auth::attempt([$loginField => $validated['login'], 'password' => $validated['password']])) {
|
||||||
|
return back()
|
||||||
|
->withErrors(['login' => 'Login atau password tidak valid.'])
|
||||||
|
->onlyInput('login');
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->session()->regenerate();
|
||||||
|
|
||||||
|
return redirect()->intended(route('dashboard.redirect'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logout(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
Auth::logout();
|
||||||
|
|
||||||
|
$request->session()->invalidate();
|
||||||
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
|
return redirect()->route('login');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function redirectDashboard(): RedirectResponse
|
||||||
|
{
|
||||||
|
$role = Auth::user()?->role;
|
||||||
|
|
||||||
|
return match ($role) {
|
||||||
|
'admin' => redirect()->route('admin.dashboard'),
|
||||||
|
'ketua' => redirect()->route('ketua.dashboard'),
|
||||||
|
'pembina' => redirect()->route('pembina.dashboard'),
|
||||||
|
default => abort(403, 'Role tidak dikenali.'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
abstract class Controller
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Kegiatan;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class HomeController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(): View
|
||||||
|
{
|
||||||
|
$kegiatanTerbaru = Kegiatan::with('organisasi:id,nama_organisasi')
|
||||||
|
->latest('created_at')
|
||||||
|
->take(5)
|
||||||
|
->get(['id', 'organisasi_id', 'nama_kegiatan', 'status', 'tanggal_mulai', 'created_at']);
|
||||||
|
|
||||||
|
return view('welcome', compact('kegiatanTerbaru'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Ketua;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\AnggotaOrganisasi;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class AnggotaController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request): View
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$organisasi = $user->organisasiSebagaiKetua()->first();
|
||||||
|
$organisasiId = $organisasi?->id;
|
||||||
|
|
||||||
|
$search = trim((string) $request->query('search', ''));
|
||||||
|
|
||||||
|
$query = AnggotaOrganisasi::where('organisasi_id', $organisasiId);
|
||||||
|
|
||||||
|
if ($search !== '') {
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('nama', 'like', "%{$search}%")
|
||||||
|
->orWhere('kelas', 'like', "%{$search}%")
|
||||||
|
->orWhere('no_hp', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$anggota = $query->orderBy('nama')->get();
|
||||||
|
|
||||||
|
return view('ketua.anggota', compact('anggota', 'search', 'organisasi'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$organisasi = $user->organisasiSebagaiKetua()->first();
|
||||||
|
|
||||||
|
if (! $organisasi) {
|
||||||
|
return redirect()->route('ketua.anggota')
|
||||||
|
->with('error', 'Anda belum menjadi ketua di organisasi manapun.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'nama' => ['required', 'string', 'max:100'],
|
||||||
|
'kelas' => ['required', 'string', 'max:50'],
|
||||||
|
'jenis_kelamin' => ['required', 'in:L,P'],
|
||||||
|
'no_hp' => ['nullable', 'string', 'max:20'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$validated['organisasi_id'] = $organisasi->id;
|
||||||
|
|
||||||
|
AnggotaOrganisasi::create($validated);
|
||||||
|
|
||||||
|
return redirect()->route('ketua.anggota')
|
||||||
|
->with('success', 'Anggota berhasil ditambahkan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, AnggotaOrganisasi $anggota): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$organisasi = $user->organisasiSebagaiKetua()->first();
|
||||||
|
|
||||||
|
if (! $organisasi || $anggota->organisasi_id !== $organisasi->id) {
|
||||||
|
return redirect()->route('ketua.anggota')
|
||||||
|
->with('error', 'Anda tidak punya akses untuk mengubah data anggota ini.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'nama' => ['required', 'string', 'max:100'],
|
||||||
|
'kelas' => ['required', 'string', 'max:50'],
|
||||||
|
'jenis_kelamin' => ['required', 'in:L,P'],
|
||||||
|
'no_hp' => ['nullable', 'string', 'max:20'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$anggota->update($validated);
|
||||||
|
|
||||||
|
return redirect()->route('ketua.anggota')
|
||||||
|
->with('success', 'Data anggota berhasil diperbarui.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(AnggotaOrganisasi $anggota): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$organisasi = $user->organisasiSebagaiKetua()->first();
|
||||||
|
|
||||||
|
if (! $organisasi || $anggota->organisasi_id !== $organisasi->id) {
|
||||||
|
return redirect()->route('ketua.anggota')
|
||||||
|
->with('error', 'Anda tidak punya akses untuk menghapus anggota ini.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$anggota->delete();
|
||||||
|
|
||||||
|
return redirect()->route('ketua.anggota')
|
||||||
|
->with('success', 'Anggota berhasil dihapus.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Ketua;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Kegiatan;
|
||||||
|
use App\Models\Dokumentasi;
|
||||||
|
use App\Models\LaporanKegiatan;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class DashboardController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(): View
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
// Cari organisasi dimana user adalah ketua
|
||||||
|
$organisasi = $user->organisasiSebagaiKetua()->first();
|
||||||
|
$organisasiId = $organisasi?->id;
|
||||||
|
|
||||||
|
$stats = [
|
||||||
|
'total_kegiatan' => Kegiatan::where('organisasi_id', $organisasiId)->count(),
|
||||||
|
'kegiatan_disetujui_admin' => Kegiatan::where('organisasi_id', $organisasiId)->where('status', 'disetujui admin')->count(),
|
||||||
|
'kegiatan_disetujui_pembina' => Kegiatan::where('organisasi_id', $organisasiId)->where('status', 'disetujui pembina')->count(),
|
||||||
|
'kegiatan_pending' => Kegiatan::where('organisasi_id', $organisasiId)->where('status', 'pending')->count(),
|
||||||
|
'kegiatan_ditolak' => Kegiatan::where('organisasi_id', $organisasiId)->whereIn('status', ['ditolak admin', 'ditolak pembina'])->count(),
|
||||||
|
'total_dokumentasi' => Kegiatan::where('organisasi_id', $organisasiId)->where('status', 'pending')->count(),
|
||||||
|
'total_dokumentasi_items' => Dokumentasi::whereHas('kegiatan', function ($q) use ($organisasiId) {
|
||||||
|
$q->where('organisasi_id', $organisasiId);
|
||||||
|
})->count(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$kegiatanTerbaru = Kegiatan::where('organisasi_id', $organisasiId)
|
||||||
|
->with('organisasi:id,nama_organisasi')
|
||||||
|
->latest('created_at')
|
||||||
|
->take(10)
|
||||||
|
->get(['id', 'nama_kegiatan', 'status', 'tanggal_mulai', 'organisasi_id', 'created_at']);
|
||||||
|
|
||||||
|
$dokumen = Dokumentasi::whereHas('kegiatan', function ($q) use ($organisasiId) {
|
||||||
|
$q->where('organisasi_id', $organisasiId);
|
||||||
|
})
|
||||||
|
->with('kegiatan')
|
||||||
|
->latest('created_at')
|
||||||
|
->get()
|
||||||
|
->map(function ($dok) {
|
||||||
|
$filePath = storage_path('app/' . $dok->file_dokumentasi);
|
||||||
|
return (object) [
|
||||||
|
'id' => 'dok_' . $dok->id,
|
||||||
|
'nama_file' => basename($dok->file_dokumentasi ?? ''),
|
||||||
|
'ukuran' => file_exists($filePath) ? filesize($filePath) : 0,
|
||||||
|
'created_at' => $dok->created_at,
|
||||||
|
'kegiatan' => $dok->kegiatan,
|
||||||
|
'type' => 'dokumentasi',
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$laporan = LaporanKegiatan::whereHas('kegiatan', function ($q) use ($organisasiId) {
|
||||||
|
$q->where('organisasi_id', $organisasiId);
|
||||||
|
})
|
||||||
|
->with('kegiatan')
|
||||||
|
->latest('created_at')
|
||||||
|
->get()
|
||||||
|
->map(function ($laporan) {
|
||||||
|
$filePath = storage_path('app/' . $laporan->file_laporan);
|
||||||
|
return (object) [
|
||||||
|
'id' => 'laporan_' . $laporan->id,
|
||||||
|
'nama_file' => basename($laporan->file_laporan ?? ''),
|
||||||
|
'ukuran' => file_exists($filePath) ? filesize($filePath) : 0,
|
||||||
|
'created_at' => $laporan->created_at,
|
||||||
|
'kegiatan' => $laporan->kegiatan,
|
||||||
|
'type' => 'laporan',
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gabungkan dokumentasi dan laporan, sort by created_at, ambil 3 terbaru
|
||||||
|
$dokumentasiTerbaru = $dokumen->concat($laporan)
|
||||||
|
->sortByDesc('created_at')
|
||||||
|
->take(3)
|
||||||
|
->values();
|
||||||
|
|
||||||
|
// Jadwal terdekat (kegiatan dengan tanggal mulai mendatang)
|
||||||
|
$jadwalMendatang = Kegiatan::where('organisasi_id', $organisasiId)
|
||||||
|
->where('tanggal_mulai', '>=', now())
|
||||||
|
->orderBy('tanggal_mulai', 'asc')
|
||||||
|
->take(5)
|
||||||
|
->get(['id', 'nama_kegiatan', 'tanggal_mulai', 'tempat']);
|
||||||
|
|
||||||
|
// Data pengajuan kegiatan per bulan (12 bulan terakhir)
|
||||||
|
$kegiatanPerBulan = Kegiatan::where('organisasi_id', $organisasiId)
|
||||||
|
->where('created_at', '>=', now()->subMonths(11)->startOfMonth())
|
||||||
|
->selectRaw('DATE_FORMAT(created_at, "%Y-%m") as bulan, COUNT(*) as jumlah')
|
||||||
|
->groupByRaw('DATE_FORMAT(created_at, "%Y-%m")')
|
||||||
|
->orderBy('bulan')
|
||||||
|
->get()
|
||||||
|
->pluck('jumlah', 'bulan')
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
// Siapkan data chart untuk 12 bulan
|
||||||
|
$chartLabels = [];
|
||||||
|
$chartData = [];
|
||||||
|
for ($i = 11; $i >= 0; $i--) {
|
||||||
|
$bulan = now()->subMonths($i)->format('Y-m');
|
||||||
|
$chartLabels[] = now()->subMonths($i)->format('M Y');
|
||||||
|
$chartData[] = $kegiatanPerBulan[$bulan] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('ketua.dashboard', compact('stats', 'kegiatanTerbaru', 'dokumentasiTerbaru', 'jadwalMendatang', 'chartLabels', 'chartData'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,287 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Ketua;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Dokumentasi;
|
||||||
|
use App\Models\Kegiatan;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class DokumentasiController extends Controller
|
||||||
|
{
|
||||||
|
private const DIRECTORY = 'dokumentasi-kegiatan';
|
||||||
|
|
||||||
|
public function __invoke(Request $request): View
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$organisasi = $user->organisasiSebagaiKetua()->first();
|
||||||
|
$organisasiId = $organisasi?->id;
|
||||||
|
|
||||||
|
$filterNamaKegiatan = trim((string) $request->query('nama_kegiatan', ''));
|
||||||
|
$search = trim((string) $request->query('search', ''));
|
||||||
|
|
||||||
|
$dokumentasi = Dokumentasi::with('kegiatan:id,nama_kegiatan')
|
||||||
|
->whereHas('kegiatan', function ($query) use ($organisasiId) {
|
||||||
|
$query->where('organisasi_id', $organisasiId);
|
||||||
|
})
|
||||||
|
|
||||||
|
->when($filterNamaKegiatan !== '', function ($query) use ($filterNamaKegiatan) {
|
||||||
|
$query->whereHas('kegiatan', function ($kegiatanQuery) use ($filterNamaKegiatan) {
|
||||||
|
$kegiatanQuery->where('nama_kegiatan', 'like', '%' . $filterNamaKegiatan . '%');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
->when($search !== '', function ($query) use ($search) {
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
|
||||||
|
$q->where('keterangan', 'like', "%{$search}%")
|
||||||
|
|
||||||
|
->orWhereHas('kegiatan', function ($kegiatanQuery) use ($search) {
|
||||||
|
$kegiatanQuery->where('nama_kegiatan', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
->latest('id')
|
||||||
|
|
||||||
|
->get([
|
||||||
|
'id',
|
||||||
|
'kegiatan_id',
|
||||||
|
'file_dokumentasi',
|
||||||
|
'keterangan'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$kegiatanList = Kegiatan::where('organisasi_id', $organisasiId)
|
||||||
|
|
||||||
|
->where('status', 'disetujui admin')
|
||||||
|
|
||||||
|
->orderBy('nama_kegiatan')
|
||||||
|
|
||||||
|
->get([
|
||||||
|
'id',
|
||||||
|
'nama_kegiatan'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view(
|
||||||
|
'ketua.dokumentasi',
|
||||||
|
compact(
|
||||||
|
'dokumentasi',
|
||||||
|
'kegiatanList',
|
||||||
|
'filterNamaKegiatan',
|
||||||
|
'search'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$organisasi = $user->organisasiSebagaiKetua()->first();
|
||||||
|
|
||||||
|
$organisasiId = $organisasi?->id;
|
||||||
|
|
||||||
|
if (! $organisasiId) {
|
||||||
|
return redirect()
|
||||||
|
->route('ketua.dokumentasi')
|
||||||
|
->with(
|
||||||
|
'error',
|
||||||
|
'Anda belum menjadi ketua di organisasi manapun.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = $request->file('file_dokumentasi', []);
|
||||||
|
|
||||||
|
$totalFiles = count($files);
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'kegiatan_id' => [
|
||||||
|
'required',
|
||||||
|
'integer',
|
||||||
|
Rule::exists('kegiatan', 'id')->where(
|
||||||
|
fn ($query) => $query
|
||||||
|
->where('organisasi_id', $organisasiId)
|
||||||
|
->where('status', 'disetujui admin')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
'keterangan' => [
|
||||||
|
'nullable',
|
||||||
|
'array',
|
||||||
|
],
|
||||||
|
|
||||||
|
'keterangan.*' => [
|
||||||
|
'nullable',
|
||||||
|
'string'
|
||||||
|
],
|
||||||
|
|
||||||
|
'file_dokumentasi' => [
|
||||||
|
'required',
|
||||||
|
'array',
|
||||||
|
'min:1'
|
||||||
|
],
|
||||||
|
|
||||||
|
'file_dokumentasi.*' => [
|
||||||
|
'required',
|
||||||
|
'image',
|
||||||
|
'mimes:jpg,jpeg,png,webp',
|
||||||
|
'max:5120'
|
||||||
|
],
|
||||||
|
|
||||||
|
]);
|
||||||
|
|
||||||
|
$folder = self::DIRECTORY . '/' . Str::slug($organisasi->nama_organisasi);
|
||||||
|
|
||||||
|
foreach ($files as $index => $file) {
|
||||||
|
|
||||||
|
$filename = time() . '_' . $file->getClientOriginalName();
|
||||||
|
|
||||||
|
$storedPath = $file->storeAs(
|
||||||
|
$folder,
|
||||||
|
$filename,
|
||||||
|
'public'
|
||||||
|
);
|
||||||
|
|
||||||
|
Dokumentasi::create([
|
||||||
|
'kegiatan_id' => $validated['kegiatan_id'],
|
||||||
|
'keterangan' => $validated['keterangan'][$index] ?? null,
|
||||||
|
'file_dokumentasi' => $storedPath,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('ketua.dokumentasi')
|
||||||
|
->with(
|
||||||
|
'success',
|
||||||
|
'Dokumentasi berhasil diunggah.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(
|
||||||
|
Request $request,
|
||||||
|
Dokumentasi $dokumentasi
|
||||||
|
): RedirectResponse {
|
||||||
|
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$organisasi = $user->organisasiSebagaiKetua()->first();
|
||||||
|
|
||||||
|
$organisasiId = $organisasi?->id;
|
||||||
|
|
||||||
|
$isAuthorized = Kegiatan::where('id', $dokumentasi->kegiatan_id)
|
||||||
|
->where('organisasi_id', $organisasiId)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (! $isAuthorized) {
|
||||||
|
return redirect()
|
||||||
|
->route('ketua.dokumentasi')
|
||||||
|
->with(
|
||||||
|
'error',
|
||||||
|
'Anda tidak punya akses ke dokumentasi ini.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'kegiatan_id' => [
|
||||||
|
'required',
|
||||||
|
'integer',
|
||||||
|
Rule::exists('kegiatan', 'id')->where(
|
||||||
|
fn ($query) => $query->where('organisasi_id', $organisasiId)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
'keterangan' => [
|
||||||
|
'nullable',
|
||||||
|
'string'
|
||||||
|
],
|
||||||
|
|
||||||
|
'file_dokumentasi' => [
|
||||||
|
'nullable',
|
||||||
|
'image',
|
||||||
|
'mimes:jpg,jpeg,png,webp',
|
||||||
|
'max:5120'
|
||||||
|
],
|
||||||
|
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->hasFile('file_dokumentasi')) {
|
||||||
|
|
||||||
|
if (
|
||||||
|
$dokumentasi->file_dokumentasi &&
|
||||||
|
! str_starts_with($dokumentasi->file_dokumentasi, 'http')
|
||||||
|
) {
|
||||||
|
Storage::disk('public')
|
||||||
|
->delete($dokumentasi->file_dokumentasi);
|
||||||
|
}
|
||||||
|
|
||||||
|
$folder = self::DIRECTORY . '/' . Str::slug($organisasi->nama_organisasi);
|
||||||
|
|
||||||
|
$file = $request->file('file_dokumentasi');
|
||||||
|
|
||||||
|
$filename = time() . '_' . $file->getClientOriginalName();
|
||||||
|
|
||||||
|
$validated['file_dokumentasi'] = $file->storeAs(
|
||||||
|
$folder,
|
||||||
|
$filename,
|
||||||
|
'public'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
unset($validated['file_dokumentasi']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dokumentasi->update($validated);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('ketua.dokumentasi')
|
||||||
|
->with(
|
||||||
|
'success',
|
||||||
|
'Dokumentasi berhasil diperbarui.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(
|
||||||
|
Dokumentasi $dokumentasi
|
||||||
|
): RedirectResponse {
|
||||||
|
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$organisasi = $user->organisasiSebagaiKetua()->first();
|
||||||
|
|
||||||
|
$organisasiId = $organisasi?->id;
|
||||||
|
|
||||||
|
$isAuthorized = Kegiatan::where('id', $dokumentasi->kegiatan_id)
|
||||||
|
->where('organisasi_id', $organisasiId)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (! $isAuthorized) {
|
||||||
|
return redirect()
|
||||||
|
->route('ketua.dokumentasi')
|
||||||
|
->with(
|
||||||
|
'error',
|
||||||
|
'Anda tidak punya akses ke dokumentasi ini.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
$dokumentasi->file_dokumentasi &&
|
||||||
|
! str_starts_with($dokumentasi->file_dokumentasi, 'http')
|
||||||
|
) {
|
||||||
|
Storage::disk('public')
|
||||||
|
->delete($dokumentasi->file_dokumentasi);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dokumentasi->delete();
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('ketua.dokumentasi')
|
||||||
|
->with(
|
||||||
|
'success',
|
||||||
|
'Dokumentasi berhasil dihapus.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Ketua;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Kegiatan;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class JadwalController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(): View
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$organisasi = $user->organisasiSebagaiKetua()->first();
|
||||||
|
$organisasiId = $organisasi?->id;
|
||||||
|
|
||||||
|
$jadwalKegiatan = Kegiatan::where('organisasi_id', $organisasiId)
|
||||||
|
->whereIn('status', ['disetujui admin'])
|
||||||
|
->orderBy('tanggal_mulai')
|
||||||
|
->get([
|
||||||
|
'id',
|
||||||
|
'nama_kegiatan',
|
||||||
|
'tanggal_mulai',
|
||||||
|
'tempat',
|
||||||
|
'status',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view('ketua.jadwal', compact('jadwalKegiatan'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Ketua;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Kegiatan;
|
||||||
|
use App\Notifications\KegiatanWorkflowNotification;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class KegiatanController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request): View
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$organisasi = $user->organisasiSebagaiKetua()->with('pembinaUsers')->first();
|
||||||
|
$organisasiId = $organisasi?->id;
|
||||||
|
|
||||||
|
$search = $request->query('search', '');
|
||||||
|
|
||||||
|
$query = Kegiatan::where('organisasi_id', $organisasiId);
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('nama_kegiatan', 'like', "%{$search}%")
|
||||||
|
->orWhere('deskripsi', 'like', "%{$search}%")
|
||||||
|
->orWhere('tempat', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$kegiatan = $query->with('penanggungJawab:id,name')->latest('id')->get([
|
||||||
|
'id',
|
||||||
|
'nama_kegiatan',
|
||||||
|
'deskripsi',
|
||||||
|
'tanggal_mulai',
|
||||||
|
'tempat',
|
||||||
|
'penanggung_jawab',
|
||||||
|
'tanggal_berakhir',
|
||||||
|
'proposal',
|
||||||
|
'status',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pembinaUsers = $organisasi?->pembinaUsers ?? collect();
|
||||||
|
|
||||||
|
return view('ketua.kegiatan', compact('kegiatan', 'search', 'pembinaUsers'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$organisasi = $user->organisasiSebagaiKetua()->first();
|
||||||
|
$organisasiId = $organisasi?->id;
|
||||||
|
|
||||||
|
if (!$organisasiId) {
|
||||||
|
return redirect()->route('ketua.kegiatan')
|
||||||
|
->with('error', 'Anda belum menjadi ketua di organisasi manapun');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'nama_kegiatan' => ['required', 'string', 'max:150'],
|
||||||
|
'deskripsi' => ['nullable', 'string'],
|
||||||
|
'tanggal_mulai' => ['required', 'date'],
|
||||||
|
'tanggal_berakhir' => ['nullable', 'date', 'after_or_equal:tanggal_mulai'],
|
||||||
|
'penanggung_jawab' => ['nullable', 'integer', 'exists:users,id'],
|
||||||
|
'tempat' => ['required', 'string', 'max:150'],
|
||||||
|
'proposal' => ['nullable', 'file', 'mimes:pdf,doc,docx', 'max:5120'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// upload proposal
|
||||||
|
if ($request->hasFile('proposal')) {
|
||||||
|
|
||||||
|
// folder berdasarkan nama organisasi
|
||||||
|
$folder = 'proposal/' . Str::slug($organisasi->nama_organisasi);
|
||||||
|
|
||||||
|
$file = $request->file('proposal');
|
||||||
|
|
||||||
|
// nama file unik
|
||||||
|
$filename = time() . '_' . $file->getClientOriginalName();
|
||||||
|
|
||||||
|
// simpan file
|
||||||
|
$validated['proposal'] = $file->storeAs(
|
||||||
|
$folder,
|
||||||
|
$filename,
|
||||||
|
'public'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated['organisasi_id'] = $organisasiId;
|
||||||
|
$validated['status'] = 'pending';
|
||||||
|
|
||||||
|
$kegiatan = Kegiatan::create($validated);
|
||||||
|
|
||||||
|
$this->notifyPembinaProposalDiajukan($kegiatan);
|
||||||
|
|
||||||
|
return redirect()->route('ketua.kegiatan')
|
||||||
|
->with('success', 'Kegiatan berhasil diajukan');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, Kegiatan $kegiatan): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$organisasi = $user->organisasiSebagaiKetua()->first();
|
||||||
|
$organisasiId = $organisasi?->id;
|
||||||
|
|
||||||
|
if ($kegiatan->organisasi_id !== $organisasiId) {
|
||||||
|
return redirect()->route('ketua.kegiatan')
|
||||||
|
->with('error', 'Anda tidak punya akses untuk mengubah kegiatan ini.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'nama_kegiatan' => ['required', 'string', 'max:150'],
|
||||||
|
'deskripsi' => ['nullable', 'string'],
|
||||||
|
'tanggal_mulai' => ['required', 'date'],
|
||||||
|
'tanggal_berakhir' => ['nullable', 'date', 'after_or_equal:tanggal_mulai'],
|
||||||
|
'penanggung_jawab' => ['nullable', 'integer', 'exists:users,id'],
|
||||||
|
'tempat' => ['required', 'string', 'max:150'],
|
||||||
|
'proposal' => ['nullable', 'file', 'mimes:pdf,doc,docx', 'max:5120'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// upload proposal baru
|
||||||
|
if ($request->hasFile('proposal')) {
|
||||||
|
|
||||||
|
// hapus file lama
|
||||||
|
if ($kegiatan->proposal && !str_starts_with($kegiatan->proposal, 'http')) {
|
||||||
|
Storage::disk('public')->delete($kegiatan->proposal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// folder berdasarkan nama organisasi
|
||||||
|
$folder = 'proposal/' . Str::slug($organisasi->nama_organisasi);
|
||||||
|
|
||||||
|
$file = $request->file('proposal');
|
||||||
|
|
||||||
|
// nama file unik
|
||||||
|
$filename = time() . '_' . $file->getClientOriginalName();
|
||||||
|
|
||||||
|
// simpan file baru
|
||||||
|
$validated['proposal'] = $file->storeAs(
|
||||||
|
$folder,
|
||||||
|
$filename,
|
||||||
|
'public'
|
||||||
|
);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
unset($validated['proposal']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// jika sebelumnya ditolak
|
||||||
|
$perluDikirimUlangKePembina = in_array(
|
||||||
|
$kegiatan->status,
|
||||||
|
['ditolak admin', 'ditolak pembina'],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($perluDikirimUlangKePembina) {
|
||||||
|
$validated['status'] = 'pending';
|
||||||
|
}
|
||||||
|
|
||||||
|
$kegiatan->update($validated);
|
||||||
|
|
||||||
|
if ($perluDikirimUlangKePembina) {
|
||||||
|
$this->notifyPembinaProposalDiajukan($kegiatan->fresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('ketua.kegiatan')
|
||||||
|
->with('success', 'Kegiatan berhasil diperbarui');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Kegiatan $kegiatan): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$organisasi = $user->organisasiSebagaiKetua()->first();
|
||||||
|
$organisasiId = $organisasi?->id;
|
||||||
|
|
||||||
|
if ($kegiatan->organisasi_id !== $organisasiId) {
|
||||||
|
return redirect()->route('ketua.kegiatan')
|
||||||
|
->with('error', 'Anda tidak punya akses untuk menghapus kegiatan ini.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// hapus file proposal
|
||||||
|
if ($kegiatan->proposal && !str_starts_with($kegiatan->proposal, 'http')) {
|
||||||
|
Storage::disk('public')->delete($kegiatan->proposal);
|
||||||
|
}
|
||||||
|
|
||||||
|
$kegiatan->delete();
|
||||||
|
|
||||||
|
return redirect()->route('ketua.kegiatan')
|
||||||
|
->with('success', 'Kegiatan berhasil dihapus');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function notifyPembinaProposalDiajukan(Kegiatan $kegiatan): void
|
||||||
|
{
|
||||||
|
$organisasi = $kegiatan->organisasi()->with('pembinaUsers')->first();
|
||||||
|
|
||||||
|
$pembinaUsers = $organisasi?->pembinaUsers;
|
||||||
|
|
||||||
|
if (!$pembinaUsers || $pembinaUsers->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($pembinaUsers as $pembina) {
|
||||||
|
$pembina->notify(new KegiatanWorkflowNotification(
|
||||||
|
'Pengajuan Proposal Baru',
|
||||||
|
'Proposal kegiatan "' . $kegiatan->nama_kegiatan .
|
||||||
|
'" dari ' . ($organisasi?->nama_organisasi ?? 'organisasi') .
|
||||||
|
' menunggu validasi Anda.',
|
||||||
|
route('pembina.validasi'),
|
||||||
|
'Buka Validasi'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,385 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Ketua;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Kegiatan;
|
||||||
|
use App\Models\LaporanKegiatan;
|
||||||
|
use App\Notifications\LaporanWorkflowNotification;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
|
||||||
|
class LaporanController extends Controller
|
||||||
|
{
|
||||||
|
private const REPORT_DIRECTORY = 'laporan-kegiatan';
|
||||||
|
|
||||||
|
public function __invoke(Request $request): View
|
||||||
|
{
|
||||||
|
$organisasiId = auth()->user()?->organisasiSebagaiKetua()->first()?->id;
|
||||||
|
|
||||||
|
$search = $request->query('search', '');
|
||||||
|
|
||||||
|
$laporan = LaporanKegiatan::with([
|
||||||
|
'kegiatan:id,organisasi_id,nama_kegiatan,tanggal_mulai',
|
||||||
|
'kegiatan.organisasi:id,nama_organisasi',
|
||||||
|
])
|
||||||
|
->whereHas('kegiatan', function ($query) use ($organisasiId) {
|
||||||
|
$query->where('organisasi_id', $organisasiId);
|
||||||
|
})
|
||||||
|
|
||||||
|
->when($search !== '', function ($query) use ($search) {
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
|
||||||
|
$q->where('isi_laporan', 'like', "%{$search}%")
|
||||||
|
|
||||||
|
->orWhereHas('kegiatan', function ($kegiatanQuery) use ($search) {
|
||||||
|
$kegiatanQuery->where(
|
||||||
|
'nama_kegiatan',
|
||||||
|
'like',
|
||||||
|
"%{$search}%"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
->latest('id')
|
||||||
|
|
||||||
|
->get([
|
||||||
|
'id',
|
||||||
|
'kegiatan_id',
|
||||||
|
'isi_laporan',
|
||||||
|
'file_laporan',
|
||||||
|
'status',
|
||||||
|
'keterangan',
|
||||||
|
'created_at'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$kegiatanTersedia = Kegiatan::where('organisasi_id', $organisasiId)
|
||||||
|
|
||||||
|
->whereIn('status', [
|
||||||
|
'disetujui pembina',
|
||||||
|
'disetujui admin'
|
||||||
|
])
|
||||||
|
|
||||||
|
->orderByDesc('tanggal_mulai')
|
||||||
|
|
||||||
|
->get([
|
||||||
|
'id',
|
||||||
|
'nama_kegiatan',
|
||||||
|
'tanggal_mulai'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view(
|
||||||
|
'ketua.laporan',
|
||||||
|
compact(
|
||||||
|
'laporan',
|
||||||
|
'kegiatanTersedia',
|
||||||
|
'search'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$organisasiId = auth()->user()?->organisasiSebagaiKetua()->first()?->id;
|
||||||
|
|
||||||
|
if (! $organisasiId) {
|
||||||
|
return redirect()
|
||||||
|
->route('ketua.laporan')
|
||||||
|
->with(
|
||||||
|
'error',
|
||||||
|
'Anda belum menjadi ketua di organisasi manapun.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'kegiatan_id' => [
|
||||||
|
'required',
|
||||||
|
'integer',
|
||||||
|
'exists:kegiatan,id',
|
||||||
|
Rule::unique('laporan_kegiatan', 'kegiatan_id')
|
||||||
|
],
|
||||||
|
|
||||||
|
'isi_laporan' => [
|
||||||
|
'required',
|
||||||
|
'string'
|
||||||
|
],
|
||||||
|
|
||||||
|
'file_laporan' => [
|
||||||
|
'required',
|
||||||
|
'file',
|
||||||
|
'mimes:pdf,doc,docx',
|
||||||
|
'max:5120'
|
||||||
|
],
|
||||||
|
|
||||||
|
], [
|
||||||
|
'kegiatan_id.unique' =>
|
||||||
|
'Kegiatan ini sudah memiliki laporan.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$kegiatanValid = Kegiatan::where(
|
||||||
|
'id',
|
||||||
|
$validated['kegiatan_id']
|
||||||
|
)
|
||||||
|
|
||||||
|
->where('organisasi_id', $organisasiId)
|
||||||
|
|
||||||
|
->whereIn('status', [
|
||||||
|
'disetujui pembina',
|
||||||
|
'disetujui admin'
|
||||||
|
])
|
||||||
|
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (! $kegiatanValid) {
|
||||||
|
return redirect()
|
||||||
|
->route('ketua.laporan')
|
||||||
|
->with(
|
||||||
|
'error',
|
||||||
|
'Kegiatan tidak valid untuk unggah laporan.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$organisasi = auth()->user()?->organisasiSebagaiKetua()->first();
|
||||||
|
|
||||||
|
$folder = self::REPORT_DIRECTORY . '/' . Str::slug($organisasi->nama_organisasi);
|
||||||
|
|
||||||
|
$file = $request->file('file_laporan');
|
||||||
|
|
||||||
|
$filename = time() . '_' . $file->getClientOriginalName();
|
||||||
|
|
||||||
|
$validated['file_laporan'] = $file->storeAs(
|
||||||
|
$folder,
|
||||||
|
$filename,
|
||||||
|
'public'
|
||||||
|
);
|
||||||
|
|
||||||
|
$laporan = LaporanKegiatan::create([
|
||||||
|
'kegiatan_id' => $validated['kegiatan_id'],
|
||||||
|
'isi_laporan' => $validated['isi_laporan'],
|
||||||
|
'file_laporan' => $validated['file_laporan'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->notifyPembinaLaporanDiajukan($laporan);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('ketua.laporan')
|
||||||
|
->with(
|
||||||
|
'success',
|
||||||
|
'Laporan kegiatan berhasil diunggah.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(
|
||||||
|
Request $request,
|
||||||
|
LaporanKegiatan $laporan
|
||||||
|
): RedirectResponse {
|
||||||
|
|
||||||
|
$organisasiId = auth()->user()?->organisasiSebagaiKetua()->first()?->id;
|
||||||
|
|
||||||
|
$isAuthorized = $laporan->kegiatan()
|
||||||
|
|
||||||
|
->where('organisasi_id', $organisasiId)
|
||||||
|
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (! $isAuthorized) {
|
||||||
|
return redirect()
|
||||||
|
->route('ketua.laporan')
|
||||||
|
->with(
|
||||||
|
'error',
|
||||||
|
'Anda tidak memiliki akses ke laporan ini.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
|
||||||
|
'isi_laporan' => [
|
||||||
|
'required',
|
||||||
|
'string'
|
||||||
|
],
|
||||||
|
|
||||||
|
'file_laporan' => [
|
||||||
|
'nullable',
|
||||||
|
'file',
|
||||||
|
'mimes:pdf,doc,docx',
|
||||||
|
'max:5120'
|
||||||
|
],
|
||||||
|
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->hasFile('file_laporan')) {
|
||||||
|
|
||||||
|
if (
|
||||||
|
$laporan->file_laporan &&
|
||||||
|
! str_starts_with($laporan->file_laporan, 'http')
|
||||||
|
) {
|
||||||
|
Storage::disk('public')
|
||||||
|
->delete($laporan->file_laporan);
|
||||||
|
}
|
||||||
|
|
||||||
|
$organisasi = auth()->user()?->organisasiSebagaiKetua()->first();
|
||||||
|
|
||||||
|
$folder = self::REPORT_DIRECTORY . '/' . Str::slug($organisasi->nama_organisasi);
|
||||||
|
|
||||||
|
$file = $request->file('file_laporan');
|
||||||
|
|
||||||
|
$filename = time() . '_' . $file->getClientOriginalName();
|
||||||
|
|
||||||
|
$validated['file_laporan'] = $file->storeAs(
|
||||||
|
$folder,
|
||||||
|
$filename,
|
||||||
|
'public'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated['status'] = 'pending';
|
||||||
|
$validated['keterangan'] = null;
|
||||||
|
$laporan->update($validated);
|
||||||
|
|
||||||
|
$this->notifyPembinaLaporanDiajukan($laporan->fresh());
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('ketua.laporan')
|
||||||
|
->with(
|
||||||
|
'success',
|
||||||
|
'Laporan kegiatan berhasil diperbarui.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(
|
||||||
|
LaporanKegiatan $laporan
|
||||||
|
): RedirectResponse {
|
||||||
|
|
||||||
|
$organisasiId = auth()->user()?->organisasiSebagaiKetua()->first()?->id;
|
||||||
|
|
||||||
|
$isAuthorized = $laporan->kegiatan()
|
||||||
|
|
||||||
|
->where('organisasi_id', $organisasiId)
|
||||||
|
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (! $isAuthorized) {
|
||||||
|
return redirect()
|
||||||
|
->route('ketua.laporan')
|
||||||
|
->with(
|
||||||
|
'error',
|
||||||
|
'Anda tidak memiliki akses ke laporan ini.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
$laporan->file_laporan &&
|
||||||
|
! str_starts_with($laporan->file_laporan, 'http')
|
||||||
|
) {
|
||||||
|
Storage::disk('public')
|
||||||
|
->delete($laporan->file_laporan);
|
||||||
|
}
|
||||||
|
|
||||||
|
$laporan->delete();
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('ketua.laporan')
|
||||||
|
->with(
|
||||||
|
'success',
|
||||||
|
'Laporan kegiatan berhasil dihapus.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function download(
|
||||||
|
LaporanKegiatan $laporan
|
||||||
|
): RedirectResponse|\Symfony\Component\HttpFoundation\StreamedResponse {
|
||||||
|
|
||||||
|
$organisasiId = auth()->user()?->organisasiSebagaiKetua()->first()?->id;
|
||||||
|
|
||||||
|
$isAuthorized = $laporan->kegiatan()
|
||||||
|
->where('organisasi_id', $organisasiId)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (! $isAuthorized) {
|
||||||
|
return redirect()
|
||||||
|
->route('ketua.laporan')
|
||||||
|
->with('error', 'Anda tidak memiliki akses ke laporan ini.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $laporan->file_laporan;
|
||||||
|
|
||||||
|
if (! $file) {
|
||||||
|
return redirect()
|
||||||
|
->route('ketua.laporan')
|
||||||
|
->with('error', 'File laporan tidak tersedia.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($file, 'http')) {
|
||||||
|
return redirect()->away($file);
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidatePaths = [$file];
|
||||||
|
|
||||||
|
if (! str_contains($file, '/')) {
|
||||||
|
$candidatePaths[] = self::REPORT_DIRECTORY . '/' . $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($candidatePaths as $path) {
|
||||||
|
if (Storage::disk('public')->exists($path)) {
|
||||||
|
$fullPath = realpath(Storage::disk('public')->path($path));
|
||||||
|
|
||||||
|
if (! $fullPath || ! is_file($fullPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filename = basename($fullPath);
|
||||||
|
$mimeType = mime_content_type($fullPath) ?: 'application/octet-stream';
|
||||||
|
$fileSize = filesize($fullPath);
|
||||||
|
|
||||||
|
return response()->streamDownload(function () use ($fullPath) {
|
||||||
|
$handle = fopen($fullPath, 'rb');
|
||||||
|
while (! feof($handle)) {
|
||||||
|
echo fread($handle, 8192);
|
||||||
|
ob_flush();
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
fclose($handle);
|
||||||
|
}, $filename, [
|
||||||
|
'Content-Type' => $mimeType,
|
||||||
|
'Content-Length' => $fileSize,
|
||||||
|
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
|
||||||
|
'Cache-Control' => 'no-cache, no-store, must-revalidate',
|
||||||
|
'Pragma' => 'no-cache',
|
||||||
|
'Expires' => '0',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('ketua.laporan')
|
||||||
|
->with('error', 'File laporan tidak ditemukan di storage.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function notifyPembinaLaporanDiajukan(LaporanKegiatan $laporan): void
|
||||||
|
{
|
||||||
|
$kegiatan = $laporan->kegiatan()->with('organisasi.pembinaUsers')->first();
|
||||||
|
$organisasi = $kegiatan?->organisasi;
|
||||||
|
$pembinaUsers = $organisasi?->pembinaUsers;
|
||||||
|
|
||||||
|
if (!$pembinaUsers || $pembinaUsers->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($pembinaUsers as $pembina) {
|
||||||
|
$pembina->notify(new LaporanWorkflowNotification(
|
||||||
|
'Pengajuan Laporan Kegiatan',
|
||||||
|
'Laporan kegiatan "' . $kegiatan->nama_kegiatan .
|
||||||
|
'" dari ' . ($organisasi?->nama_organisasi ?? 'organisasi') .
|
||||||
|
' menunggu validasi Anda.',
|
||||||
|
route('pembina.laporan'),
|
||||||
|
'Buka Laporan'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Ketua;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class OrganisasiController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(): View
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$organisasi = $user->organisasiSebagaiKetua()
|
||||||
|
->with(['pembinaUsers:id,name,email,profile_photo_path', 'ketuaUsers:id,name,profile_photo_path'])
|
||||||
|
->first();
|
||||||
|
|
||||||
|
return view('ketua.organisasi', compact('organisasi'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Ketua;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class ProfilController extends Controller
|
||||||
|
{
|
||||||
|
private const PHOTO_DIRECTORY = 'profile-photos';
|
||||||
|
|
||||||
|
public function __invoke(): View
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
return view('ketua.profil', compact('user'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => ['required', 'string', 'max:100'],
|
||||||
|
'email' => ['required', 'string', 'email', 'max:100', Rule::unique('users', 'email')->ignore($user->id)],
|
||||||
|
'profile_photo' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120'],
|
||||||
|
'password' => ['nullable', 'string', 'min:8', 'confirmed'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->hasFile('profile_photo')) {
|
||||||
|
if ($user->profile_photo_path) {
|
||||||
|
Storage::disk('public')->delete($user->profile_photo_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $request->file('profile_photo');
|
||||||
|
$filename = 'ketua-' . $user->id . '-' . now()->format('YmdHis') . '.' . $file->getClientOriginalExtension();
|
||||||
|
$validated['profile_photo_path'] = $file->storeAs(self::PHOTO_DIRECTORY, $filename, 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($validated['profile_photo']);
|
||||||
|
|
||||||
|
if (!empty($validated['password'])) {
|
||||||
|
$validated['password'] = Hash::make($validated['password']);
|
||||||
|
} else {
|
||||||
|
unset($validated['password']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->update($validated);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('ketua.profil')
|
||||||
|
->with('success', 'Profil berhasil diperbarui.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class NotificationController extends Controller
|
||||||
|
{
|
||||||
|
public function readAll(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
if ($user) {
|
||||||
|
$user->unreadNotifications->markAsRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
return back();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function readOne(Request $request, string $notificationId): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
return back();
|
||||||
|
}
|
||||||
|
|
||||||
|
$notification = $user->notifications()->where('id', $notificationId)->first();
|
||||||
|
|
||||||
|
if (! $notification) {
|
||||||
|
return back()->with('error', 'Notifikasi tidak ditemukan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_null($notification->read_at)) {
|
||||||
|
$notification->markAsRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
$actionUrl = $notification->data['action_url'] ?? null;
|
||||||
|
|
||||||
|
if (is_string($actionUrl) && $actionUrl !== '') {
|
||||||
|
return redirect()->to($actionUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,353 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Pembina;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\AbsensiEkskul;
|
||||||
|
use App\Models\AnggotaOrganisasi;
|
||||||
|
use App\Models\FotoPertemuanEkskul;
|
||||||
|
use App\Models\PertemuanEkskul;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class AbsensiEkskulController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Riwayat pertemuan (halaman utama).
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request): View
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$organisasiList = $user->organisasiSebagaiPembina()->get();
|
||||||
|
$selectedOrgId = $request->query('organisasi_id') ?: $organisasiList->first()?->id;
|
||||||
|
$organisasi = $organisasiList->firstWhere('id', $selectedOrgId);
|
||||||
|
|
||||||
|
$pertemuan = PertemuanEkskul::where('organisasi_id', $selectedOrgId)
|
||||||
|
->withCount([
|
||||||
|
'absensi as hadir_count' => fn ($q) => $q->where('status', 'hadir'),
|
||||||
|
'absensi as izin_count' => fn ($q) => $q->where('status', 'izin'),
|
||||||
|
'absensi as sakit_count' => fn ($q) => $q->where('status', 'sakit'),
|
||||||
|
'absensi as alfa_count' => fn ($q) => $q->where('status', 'alfa'),
|
||||||
|
])
|
||||||
|
->orderByDesc('tanggal')
|
||||||
|
->orderByDesc('pertemuan_ke')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$totalAnggota = AnggotaOrganisasi::where('organisasi_id', $selectedOrgId)->count();
|
||||||
|
|
||||||
|
return view('pembina.absensi-ekskul.index', compact('pertemuan', 'organisasi', 'totalAnggota', 'organisasiList', 'selectedOrgId'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form input absensi pertemuan baru.
|
||||||
|
*/
|
||||||
|
public function create(Request $request): View
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$organisasiList = $user->organisasiSebagaiPembina()->get();
|
||||||
|
$selectedOrgId = $request->query('organisasi_id') ?: $organisasiList->first()?->id;
|
||||||
|
$organisasi = $organisasiList->firstWhere('id', $selectedOrgId);
|
||||||
|
|
||||||
|
$anggota = AnggotaOrganisasi::where('organisasi_id', $selectedOrgId)
|
||||||
|
->orderBy('nama')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Hitung nomor pertemuan berikutnya
|
||||||
|
$nextPertemuan = PertemuanEkskul::where('organisasi_id', $selectedOrgId)->max('pertemuan_ke') + 1;
|
||||||
|
|
||||||
|
return view('pembina.absensi-ekskul.create', compact('anggota', 'organisasi', 'nextPertemuan', 'organisasiList', 'selectedOrgId'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simpan pertemuan baru + absensi + foto.
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'organisasi_id' => ['required', 'exists:organisasi,id'],
|
||||||
|
'tanggal' => ['required', 'date'],
|
||||||
|
'pertemuan_ke' => ['required', 'integer', 'min:1'],
|
||||||
|
'semester' => ['required', 'in:Ganjil,Genap'],
|
||||||
|
'tahun_ajaran' => ['required', 'string', 'max:15'],
|
||||||
|
'deskripsi_kegiatan' => ['nullable', 'string'],
|
||||||
|
'absensi' => ['required', 'array'],
|
||||||
|
'absensi.*' => ['required', 'in:hadir,izin,sakit,alfa'],
|
||||||
|
'foto_kegiatan' => ['nullable', 'array'],
|
||||||
|
'foto_kegiatan.*' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120'],
|
||||||
|
'keterangan_foto' => ['nullable', 'array'],
|
||||||
|
'keterangan_foto.*' => ['nullable', 'string', 'max:255'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$organisasi = $user->organisasiSebagaiPembina()->firstWhere('organisasi.id', $validated['organisasi_id']);
|
||||||
|
|
||||||
|
if (! $organisasi) {
|
||||||
|
return redirect()->route('pembina.absensi-ekskul')
|
||||||
|
->with('error', 'Anda tidak memiliki akses ke organisasi ini.');
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::transaction(function () use ($validated, $organisasi, $user, $request) {
|
||||||
|
// Simpan pertemuan
|
||||||
|
$pertemuan = PertemuanEkskul::create([
|
||||||
|
'organisasi_id' => $organisasi->id,
|
||||||
|
'pembina_id' => $user->id,
|
||||||
|
'tanggal' => $validated['tanggal'],
|
||||||
|
'pertemuan_ke' => $validated['pertemuan_ke'],
|
||||||
|
'semester' => $validated['semester'],
|
||||||
|
'tahun_ajaran' => $validated['tahun_ajaran'],
|
||||||
|
'deskripsi_kegiatan' => $validated['deskripsi_kegiatan'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Simpan absensi per anggota
|
||||||
|
foreach ($validated['absensi'] as $anggotaId => $status) {
|
||||||
|
AbsensiEkskul::create([
|
||||||
|
'pertemuan_id' => $pertemuan->id,
|
||||||
|
'anggota_id' => $anggotaId,
|
||||||
|
'status' => $status,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simpan foto kegiatan (multiple)
|
||||||
|
if ($request->hasFile('foto_kegiatan')) {
|
||||||
|
$folder = 'absensi-ekskul/' . Str::slug($organisasi->nama_organisasi);
|
||||||
|
|
||||||
|
foreach ($request->file('foto_kegiatan') as $index => $file) {
|
||||||
|
$filename = time() . '_' . $index . '_' . $file->getClientOriginalName();
|
||||||
|
$storedPath = $file->storeAs($folder, $filename, 'public');
|
||||||
|
|
||||||
|
FotoPertemuanEkskul::create([
|
||||||
|
'pertemuan_id' => $pertemuan->id,
|
||||||
|
'file_path' => $storedPath,
|
||||||
|
'keterangan' => $validated['keterangan_foto'][$index] ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return redirect()->route('pembina.absensi-ekskul')
|
||||||
|
->with('success', 'Absensi pertemuan berhasil disimpan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detail pertemuan (absensi, foto, deskripsi).
|
||||||
|
*/
|
||||||
|
public function show(PertemuanEkskul $pertemuan): View
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$organisasi = $user->organisasiSebagaiPembina()->firstWhere('organisasi.id', $pertemuan->organisasi_id);
|
||||||
|
|
||||||
|
if (! $organisasi) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pertemuan->load(['absensi.anggota', 'fotoKegiatan', 'pembina']);
|
||||||
|
|
||||||
|
return view('pembina.absensi-ekskul.show', compact('pertemuan', 'organisasi'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form edit pertemuan.
|
||||||
|
*/
|
||||||
|
public function edit(PertemuanEkskul $pertemuan): View
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$organisasi = $user->organisasiSebagaiPembina()->firstWhere('organisasi.id', $pertemuan->organisasi_id);
|
||||||
|
|
||||||
|
if (! $organisasi) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pertemuan->load(['absensi', 'fotoKegiatan']);
|
||||||
|
|
||||||
|
$anggota = AnggotaOrganisasi::where('organisasi_id', $organisasi->id)
|
||||||
|
->orderBy('nama')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Map existing absensi data
|
||||||
|
$absensiMap = $pertemuan->absensi->pluck('status', 'anggota_id')->toArray();
|
||||||
|
|
||||||
|
return view('pembina.absensi-ekskul.edit', compact('pertemuan', 'organisasi', 'anggota', 'absensiMap'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update pertemuan + absensi.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, PertemuanEkskul $pertemuan): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$organisasi = $user->organisasiSebagaiPembina()->firstWhere('organisasi.id', $pertemuan->organisasi_id);
|
||||||
|
|
||||||
|
if (! $organisasi) {
|
||||||
|
return redirect()->route('pembina.absensi-ekskul')
|
||||||
|
->with('error', 'Anda tidak punya akses ke pertemuan ini.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'tanggal' => ['required', 'date'],
|
||||||
|
'pertemuan_ke' => ['required', 'integer', 'min:1'],
|
||||||
|
'semester' => ['required', 'in:Ganjil,Genap'],
|
||||||
|
'tahun_ajaran' => ['required', 'string', 'max:15'],
|
||||||
|
'deskripsi_kegiatan' => ['nullable', 'string'],
|
||||||
|
'absensi' => ['required', 'array'],
|
||||||
|
'absensi.*' => ['required', 'in:hadir,izin,sakit,alfa'],
|
||||||
|
'foto_kegiatan' => ['nullable', 'array'],
|
||||||
|
'foto_kegiatan.*' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120'],
|
||||||
|
'keterangan_foto' => ['nullable', 'array'],
|
||||||
|
'keterangan_foto.*' => ['nullable', 'string', 'max:255'],
|
||||||
|
'hapus_foto' => ['nullable', 'array'],
|
||||||
|
'hapus_foto.*' => ['integer'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::transaction(function () use ($validated, $pertemuan, $organisasi, $request) {
|
||||||
|
// Update pertemuan
|
||||||
|
$pertemuan->update([
|
||||||
|
'tanggal' => $validated['tanggal'],
|
||||||
|
'pertemuan_ke' => $validated['pertemuan_ke'],
|
||||||
|
'semester' => $validated['semester'],
|
||||||
|
'tahun_ajaran' => $validated['tahun_ajaran'],
|
||||||
|
'deskripsi_kegiatan' => $validated['deskripsi_kegiatan'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Update absensi (upsert)
|
||||||
|
foreach ($validated['absensi'] as $anggotaId => $status) {
|
||||||
|
AbsensiEkskul::updateOrCreate(
|
||||||
|
[
|
||||||
|
'pertemuan_id' => $pertemuan->id,
|
||||||
|
'anggota_id' => $anggotaId,
|
||||||
|
],
|
||||||
|
['status' => $status]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hapus foto yang ditandai
|
||||||
|
if (! empty($validated['hapus_foto'])) {
|
||||||
|
$fotosToDelete = FotoPertemuanEkskul::where('pertemuan_id', $pertemuan->id)
|
||||||
|
->whereIn('id', $validated['hapus_foto'])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($fotosToDelete as $foto) {
|
||||||
|
if ($foto->file_path && ! str_starts_with($foto->file_path, 'http')) {
|
||||||
|
Storage::disk('public')->delete($foto->file_path);
|
||||||
|
}
|
||||||
|
$foto->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tambah foto baru
|
||||||
|
if ($request->hasFile('foto_kegiatan')) {
|
||||||
|
$folder = 'absensi-ekskul/' . Str::slug($organisasi->nama_organisasi);
|
||||||
|
|
||||||
|
foreach ($request->file('foto_kegiatan') as $index => $file) {
|
||||||
|
$filename = time() . '_' . $index . '_' . $file->getClientOriginalName();
|
||||||
|
$storedPath = $file->storeAs($folder, $filename, 'public');
|
||||||
|
|
||||||
|
FotoPertemuanEkskul::create([
|
||||||
|
'pertemuan_id' => $pertemuan->id,
|
||||||
|
'file_path' => $storedPath,
|
||||||
|
'keterangan' => $validated['keterangan_foto'][$index] ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return redirect()->route('pembina.absensi-ekskul.show', $pertemuan)
|
||||||
|
->with('success', 'Data pertemuan berhasil diperbarui.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hapus pertemuan beserta absensi dan foto.
|
||||||
|
*/
|
||||||
|
public function destroy(PertemuanEkskul $pertemuan): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$organisasi = $user->organisasiSebagaiPembina()->firstWhere('organisasi.id', $pertemuan->organisasi_id);
|
||||||
|
|
||||||
|
if (! $organisasi) {
|
||||||
|
return redirect()->route('pembina.absensi-ekskul')
|
||||||
|
->with('error', 'Anda tidak punya akses ke pertemuan ini.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hapus file foto
|
||||||
|
foreach ($pertemuan->fotoKegiatan as $foto) {
|
||||||
|
if ($foto->file_path && ! str_starts_with($foto->file_path, 'http')) {
|
||||||
|
Storage::disk('public')->delete($foto->file_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pertemuan->delete(); // cascade deletes absensi + foto
|
||||||
|
|
||||||
|
return redirect()->route('pembina.absensi-ekskul')
|
||||||
|
->with('success', 'Pertemuan berhasil dihapus.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rekap kehadiran seluruh anggota.
|
||||||
|
*/
|
||||||
|
public function rekap(Request $request): View
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$organisasiList = $user->organisasiSebagaiPembina()->get();
|
||||||
|
$selectedOrgId = $request->query('organisasi_id') ?: $organisasiList->first()?->id;
|
||||||
|
$organisasi = $organisasiList->firstWhere('id', $selectedOrgId);
|
||||||
|
|
||||||
|
$semester = $request->query('semester');
|
||||||
|
$tahunAjaran = $request->query('tahun_ajaran');
|
||||||
|
|
||||||
|
// Ambil semua pertemuan (dengan filter optional)
|
||||||
|
$pertemuanQuery = PertemuanEkskul::where('organisasi_id', $selectedOrgId);
|
||||||
|
|
||||||
|
if ($semester) {
|
||||||
|
$pertemuanQuery->where('semester', $semester);
|
||||||
|
}
|
||||||
|
if ($tahunAjaran) {
|
||||||
|
$pertemuanQuery->where('tahun_ajaran', $tahunAjaran);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pertemuanIds = $pertemuanQuery->pluck('id');
|
||||||
|
$totalPertemuan = $pertemuanIds->count();
|
||||||
|
|
||||||
|
// Ambil semua anggota dengan rekap absensi
|
||||||
|
$anggota = AnggotaOrganisasi::where('organisasi_id', $selectedOrgId)
|
||||||
|
->withCount([
|
||||||
|
'absensi as hadir_count' => fn ($q) => $q->whereIn('pertemuan_id', $pertemuanIds)->where('status', 'hadir'),
|
||||||
|
'absensi as izin_count' => fn ($q) => $q->whereIn('pertemuan_id', $pertemuanIds)->where('status', 'izin'),
|
||||||
|
'absensi as sakit_count' => fn ($q) => $q->whereIn('pertemuan_id', $pertemuanIds)->where('status', 'sakit'),
|
||||||
|
'absensi as alfa_count' => fn ($q) => $q->whereIn('pertemuan_id', $pertemuanIds)->where('status', 'alfa'),
|
||||||
|
])
|
||||||
|
->orderBy('nama')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Generate list tahun ajaran & semester secara dinamis berdasarkan pertemuan yang ada
|
||||||
|
$tahunAjaranList = PertemuanEkskul::where('organisasi_id', $selectedOrgId)
|
||||||
|
->distinct()
|
||||||
|
->pluck('tahun_ajaran')
|
||||||
|
->filter()
|
||||||
|
->toArray();
|
||||||
|
sort($tahunAjaranList);
|
||||||
|
|
||||||
|
$semesterList = PertemuanEkskul::where('organisasi_id', $selectedOrgId)
|
||||||
|
->distinct()
|
||||||
|
->pluck('semester')
|
||||||
|
->filter()
|
||||||
|
->toArray();
|
||||||
|
sort($semesterList);
|
||||||
|
|
||||||
|
return view('pembina.absensi-ekskul.rekap', compact(
|
||||||
|
'anggota',
|
||||||
|
'organisasi',
|
||||||
|
'totalPertemuan',
|
||||||
|
'semester',
|
||||||
|
'tahunAjaran',
|
||||||
|
'tahunAjaranList',
|
||||||
|
'semesterList',
|
||||||
|
'organisasiList',
|
||||||
|
'selectedOrgId'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Pembina;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Dokumentasi;
|
||||||
|
use App\Models\Kegiatan;
|
||||||
|
use App\Models\Organisasi;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class DashboardController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(): View
|
||||||
|
{
|
||||||
|
$userId = auth()->id();
|
||||||
|
|
||||||
|
$organisasiIds = Organisasi::whereHas('pembinaUsers', function ($query) use ($userId) {
|
||||||
|
$query->where('users.id', $userId);
|
||||||
|
})->pluck('id');
|
||||||
|
|
||||||
|
$stats = [
|
||||||
|
'total_organisasi' => $organisasiIds->count(),
|
||||||
|
'total_kegiatan' => Kegiatan::whereIn('organisasi_id', $organisasiIds)->count(),
|
||||||
|
'kegiatan_pending' => Kegiatan::whereIn('organisasi_id', $organisasiIds)
|
||||||
|
->where('status', 'pending')
|
||||||
|
->count(),
|
||||||
|
'kegiatan_disetujui_pembina' => Kegiatan::whereIn('organisasi_id', $organisasiIds)
|
||||||
|
->where('status', 'disetujui pembina')
|
||||||
|
->count(),
|
||||||
|
'kegiatan_disetujui_admin' => Kegiatan::whereIn('organisasi_id', $organisasiIds)
|
||||||
|
->where('status', 'disetujui admin')
|
||||||
|
->count(),
|
||||||
|
'kegiatan_ditolak' => Kegiatan::whereIn('organisasi_id', $organisasiIds)
|
||||||
|
->whereIn('status', ['ditolak pembina', 'ditolak admin'])
|
||||||
|
->count(),
|
||||||
|
'total_dokumentasi' => Dokumentasi::whereHas('kegiatan', function ($query) use ($organisasiIds) {
|
||||||
|
$query->whereIn('organisasi_id', $organisasiIds);
|
||||||
|
})->count(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$kegiatanTerbaru = Kegiatan::with('organisasi:id,nama_organisasi')
|
||||||
|
->whereIn('organisasi_id', $organisasiIds)
|
||||||
|
->latest('created_at')
|
||||||
|
->take(5)
|
||||||
|
->get(['id', 'organisasi_id', 'nama_kegiatan', 'status', 'tanggal_mulai', 'created_at']);
|
||||||
|
|
||||||
|
$kegiatanPending = Kegiatan::with('organisasi:id,nama_organisasi')
|
||||||
|
->whereIn('organisasi_id', $organisasiIds)
|
||||||
|
->where('status', 'pending')
|
||||||
|
->latest('created_at')
|
||||||
|
->take(5)
|
||||||
|
->get(['id', 'organisasi_id', 'nama_kegiatan', 'status', 'tanggal_mulai', 'created_at']);
|
||||||
|
|
||||||
|
$jadwalMendatang = Kegiatan::with('organisasi:id,nama_organisasi')
|
||||||
|
->whereIn('organisasi_id', $organisasiIds)
|
||||||
|
->whereIn('status', ['disetujui pembina', 'disetujui admin'])
|
||||||
|
->where('tanggal_mulai', '>=', now()->startOfDay())
|
||||||
|
->orderBy('tanggal_mulai', 'asc')
|
||||||
|
->take(5)
|
||||||
|
->get(['id', 'organisasi_id', 'nama_kegiatan', 'status', 'tanggal_mulai', 'tempat']);
|
||||||
|
|
||||||
|
$dokumentasiTerbaru = Dokumentasi::whereHas('kegiatan', function ($query) use ($organisasiIds) {
|
||||||
|
$query->whereIn('organisasi_id', $organisasiIds);
|
||||||
|
})
|
||||||
|
->with('kegiatan:id,nama_kegiatan')
|
||||||
|
->latest('created_at')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$kegiatanPerBulan = Kegiatan::whereIn('organisasi_id', $organisasiIds)
|
||||||
|
->where('created_at', '>=', now()->subMonths(11)->startOfMonth())
|
||||||
|
->selectRaw('DATE_FORMAT(created_at, "%Y-%m") as bulan, COUNT(*) as jumlah')
|
||||||
|
->groupByRaw('DATE_FORMAT(created_at, "%Y-%m")')
|
||||||
|
->orderBy('bulan')
|
||||||
|
->get()
|
||||||
|
->pluck('jumlah', 'bulan')
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
$chartLabels = [];
|
||||||
|
$chartData = [];
|
||||||
|
for ($i = 11; $i >= 0; $i--) {
|
||||||
|
$bulan = now()->subMonths($i)->format('Y-m');
|
||||||
|
$chartLabels[] = now()->subMonths($i)->format('M Y');
|
||||||
|
$chartData[] = $kegiatanPerBulan[$bulan] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('pembina.dashboard', compact(
|
||||||
|
'stats',
|
||||||
|
'kegiatanTerbaru',
|
||||||
|
'kegiatanPending',
|
||||||
|
'jadwalMendatang',
|
||||||
|
'dokumentasiTerbaru',
|
||||||
|
'chartLabels',
|
||||||
|
'chartData',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Pembina;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Dokumentasi;
|
||||||
|
use App\Models\Kegiatan;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class DokumentasiController extends Controller
|
||||||
|
{
|
||||||
|
private const DIRECTORY = 'dokumentasi-kegiatan';
|
||||||
|
|
||||||
|
public function __invoke(Request $request): View
|
||||||
|
{
|
||||||
|
$filterNamaKegiatan = trim((string) $request->query('nama_kegiatan', ''));
|
||||||
|
$search = trim((string) $request->query('search', ''));
|
||||||
|
|
||||||
|
$dokumentasi = Dokumentasi::with([
|
||||||
|
'kegiatan:id,organisasi_id,nama_kegiatan',
|
||||||
|
'kegiatan.organisasi:id,nama_organisasi',
|
||||||
|
])
|
||||||
|
->whereHas('kegiatan.organisasi.pembinaUsers', function ($query) {
|
||||||
|
$query->where('users.id', auth()->id());
|
||||||
|
})
|
||||||
|
->when($filterNamaKegiatan !== '', function ($query) use ($filterNamaKegiatan) {
|
||||||
|
$query->whereHas('kegiatan', function ($kegiatanQuery) use ($filterNamaKegiatan) {
|
||||||
|
$kegiatanQuery->where('nama_kegiatan', 'like', '%' . $filterNamaKegiatan . '%');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->when($search !== '', function ($query) use ($search) {
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('keterangan', 'like', "%{$search}%")
|
||||||
|
->orWhereHas('kegiatan', function ($kegiatanQuery) use ($search) {
|
||||||
|
$kegiatanQuery->where('nama_kegiatan', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->latest('created_at')
|
||||||
|
->get([
|
||||||
|
'id',
|
||||||
|
'kegiatan_id',
|
||||||
|
'file_dokumentasi',
|
||||||
|
'keterangan',
|
||||||
|
'created_at',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$kegiatanList = Kegiatan::where('status', 'disetujui admin')
|
||||||
|
->whereHas('organisasi.pembinaUsers', function ($query) {
|
||||||
|
$query->where('users.id', auth()->id());
|
||||||
|
})
|
||||||
|
->orderBy('nama_kegiatan')
|
||||||
|
->get(['id', 'nama_kegiatan']);
|
||||||
|
|
||||||
|
return view('pembina.dokumentasi', compact('dokumentasi', 'kegiatanList', 'filterNamaKegiatan', 'search'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'kegiatan_id' => [
|
||||||
|
'required',
|
||||||
|
'integer',
|
||||||
|
],
|
||||||
|
'keterangan' => [
|
||||||
|
'nullable',
|
||||||
|
'array',
|
||||||
|
],
|
||||||
|
'keterangan.*' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
],
|
||||||
|
'file_dokumentasi' => [
|
||||||
|
'required',
|
||||||
|
'array',
|
||||||
|
'min:1',
|
||||||
|
],
|
||||||
|
'file_dokumentasi.*' => [
|
||||||
|
'required',
|
||||||
|
'image',
|
||||||
|
'mimes:jpg,jpeg,png,webp',
|
||||||
|
'max:5120',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$kegiatan = Kegiatan::with('organisasi')
|
||||||
|
->where('id', $validated['kegiatan_id'])
|
||||||
|
->where('status', 'disetujui admin')
|
||||||
|
->whereHas('organisasi.pembinaUsers', fn ($q) => $q->where('users.id', auth()->id()))
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $kegiatan) {
|
||||||
|
return redirect()
|
||||||
|
->route('pembina.dokumentasi')
|
||||||
|
->withErrors(['kegiatan_id' => 'Kegiatan tidak ditemukan atau tidak tersedia untuk Anda.'])
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
$folder = self::DIRECTORY . '/' . Str::slug($kegiatan->organisasi?->nama_organisasi ?? 'pembina-' . auth()->id());
|
||||||
|
|
||||||
|
foreach ($request->file('file_dokumentasi') as $index => $file) {
|
||||||
|
$filename = time() . '_' . $file->getClientOriginalName();
|
||||||
|
$storedPath = $file->storeAs($folder, $filename, 'public');
|
||||||
|
|
||||||
|
Dokumentasi::create([
|
||||||
|
'kegiatan_id' => $validated['kegiatan_id'],
|
||||||
|
'keterangan' => $validated['keterangan'][$index] ?? null,
|
||||||
|
'file_dokumentasi' => $storedPath,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('pembina.dokumentasi')
|
||||||
|
->with('success', 'Dokumentasi berhasil diunggah.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, Dokumentasi $dokumentasi): RedirectResponse
|
||||||
|
{
|
||||||
|
$isAuthorized = Kegiatan::where('id', $dokumentasi->kegiatan_id)
|
||||||
|
->whereHas('organisasi.pembinaUsers', fn ($q) => $q->where('users.id', auth()->id()))
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (! $isAuthorized) {
|
||||||
|
return redirect()
|
||||||
|
->route('pembina.dokumentasi')
|
||||||
|
->with('error', 'Anda tidak punya akses ke dokumentasi ini.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'kegiatan_id' => [
|
||||||
|
'required',
|
||||||
|
'integer',
|
||||||
|
],
|
||||||
|
'keterangan' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
],
|
||||||
|
'file_dokumentasi' => [
|
||||||
|
'nullable',
|
||||||
|
'image',
|
||||||
|
'mimes:jpg,jpeg,png,webp',
|
||||||
|
'max:5120',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$kegiatan = Kegiatan::where('id', $validated['kegiatan_id'])
|
||||||
|
->where('status', 'disetujui admin')
|
||||||
|
->whereHas('organisasi.pembinaUsers', fn ($q) => $q->where('users.id', auth()->id()))
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $kegiatan) {
|
||||||
|
return redirect()
|
||||||
|
->route('pembina.dokumentasi')
|
||||||
|
->withErrors(['kegiatan_id' => 'Kegiatan tidak ditemukan atau tidak tersedia untuk Anda.'])
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->hasFile('file_dokumentasi')) {
|
||||||
|
if (
|
||||||
|
$dokumentasi->file_dokumentasi &&
|
||||||
|
! str_starts_with($dokumentasi->file_dokumentasi, 'http')
|
||||||
|
) {
|
||||||
|
Storage::disk('public')->delete($dokumentasi->file_dokumentasi);
|
||||||
|
}
|
||||||
|
|
||||||
|
$folder = self::DIRECTORY . '/' . Str::slug($kegiatan->organisasi?->nama_organisasi ?? 'pembina-' . auth()->id());
|
||||||
|
$file = $request->file('file_dokumentasi');
|
||||||
|
$filename = time() . '_' . $file->getClientOriginalName();
|
||||||
|
$validated['file_dokumentasi'] = $file->storeAs($folder, $filename, 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
$updateData = [
|
||||||
|
'kegiatan_id' => $validated['kegiatan_id'],
|
||||||
|
'keterangan' => $validated['keterangan'] ?? $dokumentasi->keterangan,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($request->hasFile('file_dokumentasi')) {
|
||||||
|
$updateData['file_dokumentasi'] = $validated['file_dokumentasi'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$dokumentasi->update($updateData);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('pembina.dokumentasi')
|
||||||
|
->with('success', 'Dokumentasi berhasil diperbarui.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Dokumentasi $dokumentasi): RedirectResponse
|
||||||
|
{
|
||||||
|
$isAuthorized = Kegiatan::where('id', $dokumentasi->kegiatan_id)
|
||||||
|
->whereHas('organisasi.pembinaUsers', fn ($q) => $q->where('users.id', auth()->id()))
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (! $isAuthorized) {
|
||||||
|
return redirect()
|
||||||
|
->route('pembina.dokumentasi')
|
||||||
|
->with('error', 'Anda tidak punya akses ke dokumentasi ini.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
$dokumentasi->file_dokumentasi &&
|
||||||
|
! str_starts_with($dokumentasi->file_dokumentasi, 'http')
|
||||||
|
) {
|
||||||
|
Storage::disk('public')->delete($dokumentasi->file_dokumentasi);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dokumentasi->delete();
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('pembina.dokumentasi')
|
||||||
|
->with('success', 'Dokumentasi berhasil dihapus.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Pembina;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Kegiatan;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class JadwalController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(): View
|
||||||
|
{
|
||||||
|
$jadwalKegiatan = Kegiatan::with('organisasi:id,nama_organisasi')
|
||||||
|
->whereHas('organisasi.pembinaUsers', function ($query) {
|
||||||
|
$query->where('users.id', auth()->id());
|
||||||
|
})
|
||||||
|
->whereIn('status', ['disetujui pembina', 'disetujui admin'])
|
||||||
|
->orderBy('tanggal_mulai')
|
||||||
|
->get([
|
||||||
|
'id',
|
||||||
|
'organisasi_id',
|
||||||
|
'nama_kegiatan',
|
||||||
|
'tanggal_mulai',
|
||||||
|
'tempat',
|
||||||
|
'status',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view('pembina.jadwal', compact('jadwalKegiatan'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Pembina;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Kegiatan;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class KegiatanController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request): View
|
||||||
|
{
|
||||||
|
$search = trim((string) $request->query('search', ''));
|
||||||
|
|
||||||
|
$query = Kegiatan::with([
|
||||||
|
'organisasi:id,nama_organisasi',
|
||||||
|
'penanggungJawab:id,name',
|
||||||
|
])
|
||||||
|
->whereHas('organisasi.pembinaUsers', function ($query) {
|
||||||
|
$query->where('users.id', auth()->id());
|
||||||
|
});
|
||||||
|
|
||||||
|
// SEARCH
|
||||||
|
if ($search !== '') {
|
||||||
|
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
|
||||||
|
$q->where('nama_kegiatan', 'like', "%{$search}%")
|
||||||
|
->orWhere('deskripsi', 'like', "%{$search}%")
|
||||||
|
->orWhere('tempat', 'like', "%{$search}%")
|
||||||
|
->orWhereHas('organisasi', function ($organisasiQuery) use ($search) {
|
||||||
|
|
||||||
|
$organisasiQuery->where(
|
||||||
|
'nama_organisasi',
|
||||||
|
'like',
|
||||||
|
"%{$search}%"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$kegiatan = $query
|
||||||
|
->latest('id')
|
||||||
|
->get([
|
||||||
|
'id',
|
||||||
|
'organisasi_id',
|
||||||
|
'nama_kegiatan',
|
||||||
|
'deskripsi',
|
||||||
|
'tanggal_mulai',
|
||||||
|
'tempat',
|
||||||
|
'penanggung_jawab',
|
||||||
|
'tanggal_berakhir',
|
||||||
|
'proposal',
|
||||||
|
'status',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view('pembina.kegiatan', compact(
|
||||||
|
'kegiatan',
|
||||||
|
'search'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Pembina;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\LaporanKegiatan;
|
||||||
|
use App\Notifications\LaporanWorkflowNotification;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
|
class LaporanController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request): View
|
||||||
|
{
|
||||||
|
$search = $request->query('search', '');
|
||||||
|
|
||||||
|
$query = LaporanKegiatan::with([
|
||||||
|
'kegiatan:id,organisasi_id,nama_kegiatan,tanggal_mulai',
|
||||||
|
'kegiatan.organisasi:id,nama_organisasi',
|
||||||
|
])
|
||||||
|
->whereHas('kegiatan.organisasi.pembinaUsers', function ($query) {
|
||||||
|
$query->where('users.id', auth()->id());
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('isi_laporan', 'like', "%{$search}%")
|
||||||
|
->orWhereHas('kegiatan', function ($kegiatanQuery) use ($search) {
|
||||||
|
$kegiatanQuery->where('nama_kegiatan', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$laporan = $query->latest('id')->get(['id', 'kegiatan_id', 'isi_laporan', 'file_laporan', 'status', 'keterangan']);
|
||||||
|
|
||||||
|
return view('pembina.laporan', compact('laporan', 'search'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function download(LaporanKegiatan $laporan): StreamedResponse|RedirectResponse
|
||||||
|
{
|
||||||
|
$isAuthorized = $laporan->kegiatan()
|
||||||
|
->whereHas('organisasi.pembinaUsers', function ($query) {
|
||||||
|
$query->where('users.id', auth()->id());
|
||||||
|
})
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (! $isAuthorized) {
|
||||||
|
return back()->with('error', 'Anda tidak memiliki akses ke laporan ini.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $laporan->file_laporan;
|
||||||
|
|
||||||
|
if (! $file) {
|
||||||
|
return back()->with('error', 'File laporan tidak tersedia.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($file, 'http')) {
|
||||||
|
return redirect()->away($file);
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidatePaths = [$file];
|
||||||
|
if (! str_contains($file, '/')) {
|
||||||
|
$candidatePaths[] = 'laporan-kegiatan/' . $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($candidatePaths as $path) {
|
||||||
|
if (Storage::disk('public')->exists($path)) {
|
||||||
|
return Storage::disk('public')->download($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('error', 'File laporan tidak ditemukan di storage.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function approve(LaporanKegiatan $laporan): RedirectResponse
|
||||||
|
{
|
||||||
|
$isAuthorized = $laporan->kegiatan()
|
||||||
|
->whereHas('organisasi.pembinaUsers', function ($query) {
|
||||||
|
$query->where('users.id', auth()->id());
|
||||||
|
})
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (! $isAuthorized) {
|
||||||
|
return back()->with('error', 'Anda tidak memiliki akses ke laporan ini.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($laporan->status !== 'pending') {
|
||||||
|
return back()->with('error', 'Laporan sudah divalidasi sebelumnya.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$laporan->update([
|
||||||
|
'status' => 'disetujui pembina',
|
||||||
|
'keterangan' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->notifyKetuaSetelahDisetujui($laporan->fresh());
|
||||||
|
|
||||||
|
return back()->with('success', 'Laporan kegiatan telah disetujui.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reject(Request $request, LaporanKegiatan $laporan): RedirectResponse
|
||||||
|
{
|
||||||
|
$isAuthorized = $laporan->kegiatan()
|
||||||
|
->whereHas('organisasi.pembinaUsers', function ($query) {
|
||||||
|
$query->where('users.id', auth()->id());
|
||||||
|
})
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (! $isAuthorized) {
|
||||||
|
return back()->with('error', 'Anda tidak memiliki akses ke laporan ini.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($laporan->status !== 'pending') {
|
||||||
|
return back()->with('error', 'Laporan sudah divalidasi sebelumnya.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'keterangan' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
'in:Laporan belum lengkap,Format file tidak sesuai,Data dokumentasi kurang jelas',
|
||||||
|
],
|
||||||
|
'keterangan_custom' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
'max:500',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$keterangan = trim($validated['keterangan_custom'] ?? '') ?: ($validated['keterangan'] ?? null);
|
||||||
|
|
||||||
|
$laporan->update([
|
||||||
|
'status' => 'ditolak pembina',
|
||||||
|
'keterangan' => $keterangan,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->notifyKetuaSetelahDitolak($laporan->fresh());
|
||||||
|
|
||||||
|
return back()->with('success', 'Laporan kegiatan telah ditolak.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function notifyKetuaSetelahDisetujui(LaporanKegiatan $laporan): void
|
||||||
|
{
|
||||||
|
$kegiatan = $laporan->kegiatan()->with('organisasi.ketuaUsers')->first();
|
||||||
|
$organisasi = $kegiatan?->organisasi;
|
||||||
|
$ketuaUsers = $organisasi?->ketuaUsers;
|
||||||
|
|
||||||
|
if (!$ketuaUsers || $ketuaUsers->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($ketuaUsers as $ketua) {
|
||||||
|
$ketua->notify(new LaporanWorkflowNotification(
|
||||||
|
'Laporan Disetujui Pembina',
|
||||||
|
'Laporan kegiatan "' . $kegiatan->nama_kegiatan . '" telah disetujui pembina.',
|
||||||
|
route('ketua.laporan'),
|
||||||
|
'Lihat Laporan'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function notifyKetuaSetelahDitolak(LaporanKegiatan $laporan): void
|
||||||
|
{
|
||||||
|
$kegiatan = $laporan->kegiatan()->with('organisasi.ketuaUsers')->first();
|
||||||
|
$organisasi = $kegiatan?->organisasi;
|
||||||
|
$ketuaUsers = $organisasi?->ketuaUsers;
|
||||||
|
|
||||||
|
if (!$ketuaUsers || $ketuaUsers->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$keterangan = $laporan->keterangan ?: '-';
|
||||||
|
|
||||||
|
foreach ($ketuaUsers as $ketua) {
|
||||||
|
$ketua->notify(new LaporanWorkflowNotification(
|
||||||
|
'Laporan Ditolak Pembina',
|
||||||
|
'Laporan kegiatan "' . $kegiatan->nama_kegiatan . '" ditolak pembina. Keterangan: ' . $keterangan,
|
||||||
|
route('ketua.laporan'),
|
||||||
|
'Lihat Laporan'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Pembina;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Organisasi;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class OrganisasiController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(): View
|
||||||
|
{
|
||||||
|
$organisasi = Organisasi::with([
|
||||||
|
'ketuaUsers:id,name,profile_photo_path',
|
||||||
|
])
|
||||||
|
->whereHas('pembinaUsers', function ($query) {
|
||||||
|
$query->where('users.id', auth()->id());
|
||||||
|
})
|
||||||
|
->latest('id')
|
||||||
|
->get(['id', 'nama_organisasi', 'deskripsi']);
|
||||||
|
|
||||||
|
return view('pembina.organisasi', compact('organisasi'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Pembina;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class ProfilController extends Controller
|
||||||
|
{
|
||||||
|
private const PHOTO_DIRECTORY = 'profile-photos';
|
||||||
|
|
||||||
|
public function __invoke(): View
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
return view('pembina.profil', compact('user'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => ['required', 'string', 'max:100'],
|
||||||
|
'email' => ['required', 'string', 'email', 'max:100', Rule::unique('users', 'email')->ignore($user->id)],
|
||||||
|
'profile_photo' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120'],
|
||||||
|
'password' => ['nullable', 'string', 'min:8', 'confirmed'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->hasFile('profile_photo')) {
|
||||||
|
if ($user->profile_photo_path) {
|
||||||
|
Storage::disk('public')->delete($user->profile_photo_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $request->file('profile_photo');
|
||||||
|
$filename = 'pembina-' . $user->id . '-' . now()->format('YmdHis') . '.' . $file->getClientOriginalExtension();
|
||||||
|
$validated['profile_photo_path'] = $file->storeAs(self::PHOTO_DIRECTORY, $filename, 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($validated['profile_photo']);
|
||||||
|
|
||||||
|
if (!empty($validated['password'])) {
|
||||||
|
$validated['password'] = Hash::make($validated['password']);
|
||||||
|
} else {
|
||||||
|
unset($validated['password']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->update($validated);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('pembina.profil')
|
||||||
|
->with('success', 'Profil berhasil diperbarui.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Pembina;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Kegiatan;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Notifications\KegiatanWorkflowNotification;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class ValidasiController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(): View
|
||||||
|
{
|
||||||
|
$kegiatanMenungguValidasiPembina = Kegiatan::with('organisasi:id,nama_organisasi')
|
||||||
|
->where('status', 'pending')
|
||||||
|
->whereHas('organisasi.pembinaUsers', function ($query) {
|
||||||
|
$query->where('users.id', auth()->id());
|
||||||
|
})
|
||||||
|
->latest('id')
|
||||||
|
->get([
|
||||||
|
'id',
|
||||||
|
'organisasi_id',
|
||||||
|
'nama_kegiatan',
|
||||||
|
'deskripsi',
|
||||||
|
'tanggal_mulai',
|
||||||
|
'proposal',
|
||||||
|
'status',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view('pembina.validasi', compact('kegiatanMenungguValidasiPembina'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function approve(Kegiatan $kegiatan): RedirectResponse
|
||||||
|
{
|
||||||
|
if (! $this->canValidate($kegiatan)) {
|
||||||
|
return redirect()->route('pembina.validasi')->with('error', 'Kegiatan tidak ditemukan atau bukan milik organisasi binaan Anda.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($kegiatan->status !== 'pending') {
|
||||||
|
return redirect()->route('pembina.validasi')->with('error', 'Kegiatan sudah divalidasi sebelumnya.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$kegiatan->update([
|
||||||
|
'status' => 'disetujui pembina',
|
||||||
|
'keterangan' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->notifyAdminSetelahDisetujuiPembina($kegiatan->fresh());
|
||||||
|
|
||||||
|
return redirect()->route('pembina.validasi')->with('success', 'Kegiatan "' . $kegiatan->nama_kegiatan . '" telah disetujui pembina.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reject(Request $request, Kegiatan $kegiatan): RedirectResponse
|
||||||
|
{
|
||||||
|
if (! $this->canValidate($kegiatan)) {
|
||||||
|
return redirect()->route('pembina.validasi')->with('error', 'Kegiatan tidak ditemukan atau bukan milik organisasi binaan Anda.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($kegiatan->status !== 'pending') {
|
||||||
|
return redirect()->route('pembina.validasi')->with('error', 'Kegiatan sudah divalidasi sebelumnya.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'keterangan' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
'in:Proposal belum lengkap,Anggaran tidak sesuai,Deskripsi atau data kegiatan kurang jelas',
|
||||||
|
],
|
||||||
|
'keterangan_custom' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
'max:500',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$keterangan = trim($validated['keterangan_custom'] ?? '') ?: ($validated['keterangan'] ?? null);
|
||||||
|
|
||||||
|
$kegiatan->update([
|
||||||
|
'status' => 'ditolak pembina',
|
||||||
|
'keterangan' => $keterangan,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->notifyKetuaSetelahDitolakPembina($kegiatan->fresh());
|
||||||
|
|
||||||
|
return redirect()->route('pembina.validasi')->with('success', 'Kegiatan "' . $kegiatan->nama_kegiatan . '" telah ditolak pembina.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function canValidate(Kegiatan $kegiatan): bool
|
||||||
|
{
|
||||||
|
return $kegiatan->organisasi()
|
||||||
|
->whereHas('pembinaUsers', function ($query) {
|
||||||
|
$query->where('users.id', auth()->id());
|
||||||
|
})
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function notifyAdminSetelahDisetujuiPembina(Kegiatan $kegiatan): void
|
||||||
|
{
|
||||||
|
$organisasi = $kegiatan->organisasi()->first();
|
||||||
|
|
||||||
|
$admins = User::query()
|
||||||
|
->where('role', 'admin')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($admins as $admin) {
|
||||||
|
$admin->notify(new KegiatanWorkflowNotification(
|
||||||
|
'Validasi Admin Dibutuhkan',
|
||||||
|
'Kegiatan "' . $kegiatan->nama_kegiatan . '" dari ' . ($organisasi?->nama_organisasi ?? 'organisasi') . ' telah disetujui pembina dan menunggu persetujuan admin.',
|
||||||
|
route('admin.validasi'),
|
||||||
|
'Buka validasi admin'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function notifyKetuaSetelahDitolakPembina(Kegiatan $kegiatan): void
|
||||||
|
{
|
||||||
|
$organisasi = $kegiatan->organisasi()->first();
|
||||||
|
$ketuaUsers = $organisasi?->ketuaUsers;
|
||||||
|
|
||||||
|
if (! $ketuaUsers || $ketuaUsers->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$keterangan = $kegiatan->keterangan ?: '-';
|
||||||
|
|
||||||
|
foreach ($ketuaUsers as $ketua) {
|
||||||
|
$ketua->notify(new KegiatanWorkflowNotification(
|
||||||
|
'Proposal Ditolak Pembina',
|
||||||
|
'Proposal kegiatan "' . $kegiatan->nama_kegiatan . '" ditolak pembina. Keterangan: ' . $keterangan,
|
||||||
|
route('ketua.kegiatan'),
|
||||||
|
'Lihat kegiatan'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class RoleMiddleware
|
||||||
|
{
|
||||||
|
public function handle(Request $request, Closure $next, string ...$roles): Response
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
return redirect()->route('login');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! in_array($user->role, $roles, true)) {
|
||||||
|
abort(403, 'Anda tidak memiliki akses ke halaman ini.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class AbsensiEkskul extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'absensi_ekskul';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'pertemuan_id',
|
||||||
|
'anggota_id',
|
||||||
|
'status',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function pertemuan(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(PertemuanEkskul::class, 'pertemuan_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function anggota(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(AnggotaOrganisasi::class, 'anggota_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class AnggotaOrganisasi extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'anggota_organisasi';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'organisasi_id',
|
||||||
|
'nama',
|
||||||
|
'kelas',
|
||||||
|
'jenis_kelamin',
|
||||||
|
'no_hp',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function organisasi(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Organisasi::class, 'organisasi_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function absensi(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(AbsensiEkskul::class, 'anggota_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class Dokumentasi extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'dokumentasi';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'kegiatan_id',
|
||||||
|
'file_dokumentasi',
|
||||||
|
'keterangan',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function kegiatan(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Kegiatan::class, 'kegiatan_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class FotoPertemuanEkskul extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'foto_pertemuan_ekskul';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'pertemuan_id',
|
||||||
|
'file_path',
|
||||||
|
'keterangan',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function pertemuan(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(PertemuanEkskul::class, 'pertemuan_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class Kegiatan extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'kegiatan';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'organisasi_id',
|
||||||
|
'nama_kegiatan',
|
||||||
|
'deskripsi',
|
||||||
|
'tanggal_mulai',
|
||||||
|
'tempat',
|
||||||
|
'proposal',
|
||||||
|
'status',
|
||||||
|
'keterangan',
|
||||||
|
'penanggung_jawab',
|
||||||
|
'tanggal_berakhir',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'tanggal_mulai' => 'date',
|
||||||
|
'tanggal_berakhir' => 'date',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function organisasi(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Organisasi::class, 'organisasi_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function penanggungJawab(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'penanggung_jawab');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dokumentasi(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(\App\Models\Dokumentasi::class, 'kegiatan_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function laporanKegiatan(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(\App\Models\LaporanKegiatan::class, 'kegiatan_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class LaporanKegiatan extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'laporan_kegiatan';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'kegiatan_id',
|
||||||
|
'isi_laporan',
|
||||||
|
'file_laporan',
|
||||||
|
'status',
|
||||||
|
'keterangan',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function kegiatan(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Kegiatan::class, 'kegiatan_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class Organisasi extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'organisasi';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'nama_organisasi',
|
||||||
|
'deskripsi',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function pembinaUsers(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(User::class, 'organisasi_user')
|
||||||
|
->wherePivot('role', 'pembina')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ketuaUsers(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(User::class, 'organisasi_user')
|
||||||
|
->wherePivot('role', 'ketua')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function kegiatan(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Kegiatan::class, 'organisasi_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function anggota(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(AnggotaOrganisasi::class, 'organisasi_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pertemuanEkskul(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(PertemuanEkskul::class, 'organisasi_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class PertemuanEkskul extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'pertemuan_ekskul';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'organisasi_id',
|
||||||
|
'pembina_id',
|
||||||
|
'tanggal',
|
||||||
|
'pertemuan_ke',
|
||||||
|
'semester',
|
||||||
|
'tahun_ajaran',
|
||||||
|
'deskripsi_kegiatan',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'tanggal' => 'date',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function organisasi(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Organisasi::class, 'organisasi_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pembina(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'pembina_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function absensi(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(AbsensiEkskul::class, 'pertemuan_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fotoKegiatan(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(FotoPertemuanEkskul::class, 'pertemuan_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class Session extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'sessions';
|
||||||
|
|
||||||
|
protected $primaryKey = 'id';
|
||||||
|
|
||||||
|
public $incrementing = false;
|
||||||
|
|
||||||
|
protected $keyType = 'string';
|
||||||
|
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'id',
|
||||||
|
'user_id',
|
||||||
|
'ip_address',
|
||||||
|
'user_agent',
|
||||||
|
'payload',
|
||||||
|
'last_activity',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'user_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
|
||||||
|
#[Fillable(['name', 'email', 'password', 'role', 'profile_photo_path'])]
|
||||||
|
#[Hidden(['password', 'remember_token'])]
|
||||||
|
class User extends Authenticatable
|
||||||
|
{
|
||||||
|
use HasFactory, Notifiable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email_verified_at' => 'datetime',
|
||||||
|
'password' => 'hashed',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function organisasiSebagaiPembina(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Organisasi::class, 'organisasi_user')
|
||||||
|
->wherePivot('role', 'pembina')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function organisasiSebagaiKetua(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Organisasi::class, 'organisasi_user')
|
||||||
|
->wherePivot('role', 'ketua')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sessions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Session::class, 'user_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Notifications;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
|
||||||
|
class KegiatanWorkflowNotification extends Notification
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $title,
|
||||||
|
private readonly string $message,
|
||||||
|
private readonly ?string $actionUrl = null,
|
||||||
|
private readonly ?string $actionLabel = null
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the notification's delivery channels.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function via(object $notifiable): array
|
||||||
|
{
|
||||||
|
return ['database', 'mail'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the mail representation of the notification.
|
||||||
|
*/
|
||||||
|
public function toMail(object $notifiable): MailMessage
|
||||||
|
{
|
||||||
|
return (new MailMessage)
|
||||||
|
->view('emails.kegiatan-workflow', [
|
||||||
|
'notifiable' => $notifiable,
|
||||||
|
'title' => $this->title,
|
||||||
|
'contentMessage' => $this->message,
|
||||||
|
'actionUrl' => $this->actionUrl,
|
||||||
|
'actionLabel' => $this->actionLabel ?? 'Buka',
|
||||||
|
])
|
||||||
|
->subject($this->title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the array representation of the notification.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(object $notifiable): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'title' => $this->title,
|
||||||
|
'message' => $this->message,
|
||||||
|
'action_url' => $this->actionUrl,
|
||||||
|
'action_label' => $this->actionLabel ?? 'Lihat detail',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Notifications;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
|
||||||
|
class LaporanWorkflowNotification extends Notification
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $title,
|
||||||
|
private readonly string $message,
|
||||||
|
private readonly ?string $actionUrl = null,
|
||||||
|
private readonly ?string $actionLabel = null
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the notification's delivery channels.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function via(object $notifiable): array
|
||||||
|
{
|
||||||
|
return ['database', 'mail'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the mail representation of the notification.
|
||||||
|
*/
|
||||||
|
public function toMail(object $notifiable): MailMessage
|
||||||
|
{
|
||||||
|
return (new MailMessage)
|
||||||
|
->view('emails.kegiatan-workflow', [
|
||||||
|
'notifiable' => $notifiable,
|
||||||
|
'title' => $this->title,
|
||||||
|
'contentMessage' => $this->message,
|
||||||
|
'actionUrl' => $this->actionUrl,
|
||||||
|
'actionLabel' => $this->actionLabel ?? 'Buka',
|
||||||
|
])
|
||||||
|
->subject($this->title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the array representation of the notification.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(object $notifiable): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'title' => $this->title,
|
||||||
|
'message' => $this->message,
|
||||||
|
'action_url' => $this->actionUrl,
|
||||||
|
'action_label' => $this->actionLabel ?? 'Lihat detail',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Providers\AppServiceProvider;
|
||||||
|
|
||||||
|
return [
|
||||||
|
AppServiceProvider::class,
|
||||||
|
];
|
||||||
|
|
@ -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.3",
|
||||||
|
"barryvdh/laravel-dompdf": "^3.1",
|
||||||
|
"laravel/framework": "^13.0",
|
||||||
|
"laravel/tinker": "^3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"fakerphp/faker": "^1.23",
|
||||||
|
"laravel/pail": "^1.2.5",
|
||||||
|
"laravel/pint": "^1.27",
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"nunomaduro/collision": "^8.6",
|
||||||
|
"phpunit/phpunit": "^12.5.12"
|
||||||
|
},
|
||||||
|
"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 --timeout=0\" \"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
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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', 'id'),
|
||||||
|
|
||||||
|
'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'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
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', 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),
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
<?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-'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Serializable Classes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the classes that can be unserialized from cache
|
||||||
|
| storage. By default, no PHP classes will be unserialized from your
|
||||||
|
| cache to prevent gadget chain attacks if your APP_KEY is leaked.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'serializable_classes' => false,
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Pdo\Mysql;
|
||||||
|
|
||||||
|
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([
|
||||||
|
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : 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([
|
||||||
|
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : 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' => env('DB_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),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -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' => rtrim(env('APP_URL', 'http://localhost'), '/').'/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'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -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', env('APP_NAME', 'Laravel')),
|
||||||
|
'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'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -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', env('APP_NAME', 'Laravel')),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -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',
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -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'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,233 @@
|
||||||
|
<?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 without 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),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Serialization
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value controls the serialization strategy for session data, which
|
||||||
|
| is JSON by default. Setting this to "php" allows the storage of PHP
|
||||||
|
| objects in the session but can make an application vulnerable to
|
||||||
|
| "gadget chain" serialization attacks if the APP_KEY is leaked.
|
||||||
|
|
|
||||||
|
| Supported: "json", "php"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'serialization' => 'json',
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
*.sqlite*
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends Factory<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(),
|
||||||
|
'email_verified_at' => now(),
|
||||||
|
'password' => static::$password ??= Hash::make('password'),
|
||||||
|
'remember_token' => Str::random(10),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the model's email address should be unverified.
|
||||||
|
*/
|
||||||
|
public function unverified(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'email_verified_at' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(): void
|
||||||
|
{
|
||||||
|
Schema::create('users', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name', 100);
|
||||||
|
$table->string('email', 100)->unique();
|
||||||
|
$table->string('password', 255);
|
||||||
|
$table->enum('role', ['admin', 'ketua', 'pembina'])->default('ketua');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('users');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?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('organisasi', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('nama_organisasi', 100);
|
||||||
|
$table->text('deskripsi')->nullable();
|
||||||
|
|
||||||
|
$table->foreignId('pembina_id')
|
||||||
|
->nullable()
|
||||||
|
->constrained('users')
|
||||||
|
->nullOnDelete();
|
||||||
|
|
||||||
|
$table->foreignId('ketua_id')
|
||||||
|
->nullable()
|
||||||
|
->constrained('users')
|
||||||
|
->nullOnDelete();
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('organisasi');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?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('kegiatan', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->foreignId('organisasi_id')
|
||||||
|
->constrained('organisasi')
|
||||||
|
->cascadeOnDelete();
|
||||||
|
|
||||||
|
$table->string('nama_kegiatan', 150);
|
||||||
|
$table->text('deskripsi')->nullable();
|
||||||
|
$table->date('tanggal_mulai')->nullable();
|
||||||
|
$table->string('tempat', 150)->nullable();
|
||||||
|
$table->string('proposal', 255)->nullable();
|
||||||
|
$table->string('keterangan')->nullable();
|
||||||
|
|
||||||
|
$table->enum('status', ['pending', 'disetujui pembina', 'disetujui admin', 'ditolak pembina', 'ditolak admin'])
|
||||||
|
->default('pending');
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('kegiatan');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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('dokumentasi', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->foreignId('kegiatan_id')
|
||||||
|
->constrained('kegiatan')
|
||||||
|
->cascadeOnDelete();
|
||||||
|
|
||||||
|
$table->string('file_dokumentasi', 255)->nullable();
|
||||||
|
$table->text('keterangan')->nullable();
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('dokumentasi');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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(): void
|
||||||
|
{
|
||||||
|
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('sessions');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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(): void
|
||||||
|
{
|
||||||
|
Schema::create('laporan_kegiatan', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->foreignId('kegiatan_id')
|
||||||
|
->constrained('kegiatan')
|
||||||
|
->cascadeOnDelete();
|
||||||
|
|
||||||
|
$table->text('isi_laporan');
|
||||||
|
$table->string('file_laporan', 255)->nullable();
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('laporan_kegiatan');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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(): void
|
||||||
|
{
|
||||||
|
Schema::create('notifications', function (Blueprint $table) {
|
||||||
|
$table->uuid('id')->primary();
|
||||||
|
$table->string('type');
|
||||||
|
$table->morphs('notifiable');
|
||||||
|
$table->text('data');
|
||||||
|
$table->timestamp('read_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('notifications');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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): void {
|
||||||
|
$table->string('profile_photo_path', 255)->nullable()->after('role');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn('profile_photo_path');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?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('kegiatan', function (Blueprint $table) {
|
||||||
|
$table->foreignId('penanggung_jawab')->nullable()->after('tempat')->constrained('users')->nullOnDelete();
|
||||||
|
$table->date('tanggal_berakhir')->nullable()->after('penanggung_jawab');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('kegiatan', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['penanggung_jawab']);
|
||||||
|
$table->dropColumn(['penanggung_jawab', 'tanggal_berakhir']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// 1. Create pivot table
|
||||||
|
Schema::create('organisasi_user', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('organisasi_id')->constrained('organisasi')->cascadeOnDelete();
|
||||||
|
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||||
|
$table->enum('role', ['ketua', 'pembina']);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['organisasi_id', 'user_id', 'role']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Migrate existing data from old columns to pivot table
|
||||||
|
$organisasiRows = DB::table('organisasi')
|
||||||
|
->select('id', 'pembina_id', 'ketua_id')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($organisasiRows as $row) {
|
||||||
|
if ($row->pembina_id) {
|
||||||
|
DB::table('organisasi_user')->insert([
|
||||||
|
'organisasi_id' => $row->id,
|
||||||
|
'user_id' => $row->pembina_id,
|
||||||
|
'role' => 'pembina',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if ($row->ketua_id) {
|
||||||
|
DB::table('organisasi_user')->insert([
|
||||||
|
'organisasi_id' => $row->id,
|
||||||
|
'user_id' => $row->ketua_id,
|
||||||
|
'role' => 'ketua',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Drop old foreign key columns
|
||||||
|
Schema::table('organisasi', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['pembina_id']);
|
||||||
|
$table->dropForeign(['ketua_id']);
|
||||||
|
$table->dropColumn(['pembina_id', 'ketua_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// Re-add old columns
|
||||||
|
Schema::table('organisasi', function (Blueprint $table) {
|
||||||
|
$table->foreignId('pembina_id')->nullable()->constrained('users')->nullOnDelete();
|
||||||
|
$table->foreignId('ketua_id')->nullable()->constrained('users')->nullOnDelete();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Migrate data back from pivot
|
||||||
|
$pivotRows = DB::table('organisasi_user')->get();
|
||||||
|
foreach ($pivotRows as $row) {
|
||||||
|
if ($row->role === 'pembina') {
|
||||||
|
DB::table('organisasi')->where('id', $row->organisasi_id)->update(['pembina_id' => $row->user_id]);
|
||||||
|
}
|
||||||
|
if ($row->role === 'ketua') {
|
||||||
|
DB::table('organisasi')->where('id', $row->organisasi_id)->update(['ketua_id' => $row->user_id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::dropIfExists('organisasi_user');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('anggota_organisasi', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->foreignId('organisasi_id')
|
||||||
|
->constrained('organisasi')
|
||||||
|
->cascadeOnDelete();
|
||||||
|
|
||||||
|
$table->string('nama', 100);
|
||||||
|
$table->string('kelas', 50)->nullable();
|
||||||
|
$table->enum('jenis_kelamin', ['L', 'P'])->nullable();
|
||||||
|
$table->string('no_hp', 20)->nullable();
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('anggota_organisasi');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('pertemuan_ekskul', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->foreignId('organisasi_id')
|
||||||
|
->constrained('organisasi')
|
||||||
|
->cascadeOnDelete();
|
||||||
|
|
||||||
|
$table->foreignId('pembina_id')
|
||||||
|
->nullable()
|
||||||
|
->constrained('users')
|
||||||
|
->nullOnDelete();
|
||||||
|
|
||||||
|
$table->date('tanggal');
|
||||||
|
$table->unsignedInteger('pertemuan_ke');
|
||||||
|
$table->string('semester', 10);
|
||||||
|
$table->string('tahun_ajaran', 15);
|
||||||
|
$table->text('deskripsi_kegiatan')->nullable();
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('pertemuan_ekskul');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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(): void
|
||||||
|
{
|
||||||
|
Schema::create('foto_pertemuan_ekskul', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->foreignId('pertemuan_id')
|
||||||
|
->constrained('pertemuan_ekskul')
|
||||||
|
->cascadeOnDelete();
|
||||||
|
|
||||||
|
$table->string('file_path', 255);
|
||||||
|
$table->string('keterangan', 255)->nullable();
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('foto_pertemuan_ekskul');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('absensi_ekskul', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->foreignId('pertemuan_id')
|
||||||
|
->constrained('pertemuan_ekskul')
|
||||||
|
->cascadeOnDelete();
|
||||||
|
|
||||||
|
$table->foreignId('anggota_id')
|
||||||
|
->constrained('anggota_organisasi')
|
||||||
|
->cascadeOnDelete();
|
||||||
|
|
||||||
|
$table->enum('status', ['hadir', 'izin', 'sakit', 'alfa'])
|
||||||
|
->default('alfa');
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['pertemuan_id', 'anggota_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('absensi_ekskul');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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(): void
|
||||||
|
{
|
||||||
|
Schema::table('laporan_kegiatan', function (Blueprint $table) {
|
||||||
|
$table->string('status')->default('pending'); // pending, disetujui pembina, ditolak pembina
|
||||||
|
$table->text('keterangan')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('laporan_kegiatan', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['status', 'keterangan']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
# Dokumentasi Backend
|
||||||
|
|
||||||
|
## Ringkasan
|
||||||
|
|
||||||
|
- Framework: Laravel (PHP)
|
||||||
|
- Struktur: MVC (Models, Views, Controllers) dengan routes di `routes/web.php`.
|
||||||
|
- Backend ini memisahkan akses berdasarkan peran pengguna, jadi tiap role hanya melihat fitur yang memang menjadi tanggung jawabnya.
|
||||||
|
|
||||||
|
## Arsitektur Utama
|
||||||
|
|
||||||
|
- Namespace utama: `App\\Http\\Controllers` untuk controller, `App\\Models` untuk model.
|
||||||
|
- Shared middleware: `auth`, `role` untuk membatasi akses (admin, ketua, pembina).
|
||||||
|
- `auth` memastikan pengguna sudah login sebelum masuk ke area internal.
|
||||||
|
- `role` membedakan hak akses berdasarkan jabatan, misalnya admin untuk pengelolaan data utama, ketua untuk operasional organisasi, dan pembina untuk pengawasan serta validasi.
|
||||||
|
|
||||||
|
## Routes & Grup
|
||||||
|
|
||||||
|
- Guest routes: login, register (`routes/web.php`).
|
||||||
|
- Authenticated routes: grouped di bawah `auth` middleware dengan redirect dashboard dan notifikasi.
|
||||||
|
- Role-based groups: `admin`, `ketua`, `pembina` — masing-masing punya route resource untuk `organisasi`, `kegiatan`, `dokumentasi`, `laporan`, validasi, dsb.
|
||||||
|
|
||||||
|
Secara sederhana, alurnya seperti ini: pengguna masuk lewat login, sistem menentukan perannya, lalu pengguna diarahkan ke dashboard yang sesuai. Setelah itu route yang tersedia juga menyesuaikan role yang dimiliki.
|
||||||
|
|
||||||
|
Contoh file route: [routes/web.php](routes/web.php)
|
||||||
|
|
||||||
|
## Models (ringkasan)
|
||||||
|
|
||||||
|
- `User` — model pengguna; lokasi: [app/Models/User.php](app/Models/User.php).
|
||||||
|
- `Organisasi` — data organisasi; lokasi: [app/Models/Organisasi.php](app/Models/Organisasi.php).
|
||||||
|
- `Kegiatan` — data kegiatan/agenda; lokasi: [app/Models/Kegiatan.php](app/Models/Kegiatan.php).
|
||||||
|
- `Dokumentasi` — unggahan foto/dokumentasi kegiatan; lokasi: [app/Models/Dokumentasi.php](app/Models/Dokumentasi.php).
|
||||||
|
- `LaporanKegiatan` — laporan kegiatan dan attachment; lokasi: [app/Models/LaporanKegiatan.php](app/Models/LaporanKegiatan.php).
|
||||||
|
- `Session` — sesi/timeline kegiatan; lokasi: [app/Models/Session.php](app/Models/Session.php).
|
||||||
|
|
||||||
|
Untuk detail kolom, lihat migrations di `database/migrations/`.
|
||||||
|
|
||||||
|
Relasi penting yang terlihat di model adalah `Kegiatan` yang terhubung ke `Organisasi`, lalu memiliki relasi ke `Dokumentasi` dan `LaporanKegiatan`. Artinya satu kegiatan bisa punya banyak dokumentasi dan beberapa laporan pendukung.
|
||||||
|
|
||||||
|
Contoh isi model `Kegiatan` juga menunjukkan field yang sering dipakai backend, seperti nama kegiatan, tanggal mulai, tempat, proposal, status, dan keterangan.
|
||||||
|
|
||||||
|
## Migrations & Skema
|
||||||
|
|
||||||
|
- Semua migration ada di `database/migrations/` (contoh: `create_users_table`, `create_organisasi_table`, `create_kegiatan_table`, `create_dokumentasi_table`, `create_laporan_kegiatan_table`, `create_sessions_table`, `create_notifications_table`).
|
||||||
|
- Jalankan: `php artisan migrate` untuk membuat tabel.
|
||||||
|
- Migration di project ini menjadi sumber utama untuk memahami struktur data karena dari sanalah nama tabel, kolom, tipe data, dan relasi dasar disusun.
|
||||||
|
|
||||||
|
## Controllers & Alur Kerja
|
||||||
|
|
||||||
|
- Controllers dikelompokkan menurut role:
|
||||||
|
- `App\\Http\\Controllers\\Admin\\*` — manajemen pengguna, organisasi, validasi, laporan, dokumentasi.
|
||||||
|
- `App\\Http\\Controllers\\Ketua\\*` — fitur yang diakses ketua organisasi (CRUD kegiatan, dokumentasi, laporan, jadwal).
|
||||||
|
- `App\\Http\\Controllers\\Pembina\\*` — fitur untuk pembina, termasuk validasi dan pemantauan.
|
||||||
|
- Controller otentikasi: `App\\Http\\Controllers\\AuthController.php` (login, register, logout, redirect dashboard).
|
||||||
|
- Notifikasi: `App\\Http\\Controllers\\NotificationController.php` — baca notifikasi (read-all, read-one).
|
||||||
|
|
||||||
|
Mayoritas controller di sini mengikuti pola CRUD: menampilkan daftar data, menyimpan data baru, memperbarui data lama, dan menghapus data. Beberapa controller juga punya aksi tambahan seperti validasi approve/reject dan ekspor PDF untuk laporan.
|
||||||
|
|
||||||
|
Lihat folder controller untuk implementasi: [app/Http/Controllers](app/Http/Controllers)
|
||||||
|
|
||||||
|
## Notifikasi & Workflow
|
||||||
|
|
||||||
|
- Notifikasi disimpan di tabel `notifications` (migration tersedia) dan dikirim via kelas notifikasi di `app/Notifications`.
|
||||||
|
- Ada workflow validasi kegiatan: route `approve`/`reject` pada controller `ValidasiController` untuk Admin/Pembina.
|
||||||
|
- Workflow validasi ini penting karena status kegiatan bisa berubah berdasarkan keputusan pihak yang berwenang, sehingga data yang tampil ke pengguna lain ikut menyesuaikan.
|
||||||
|
|
||||||
|
## File Storage
|
||||||
|
|
||||||
|
- File upload (dokumentasi, laporan) disimpan di `storage/` dan diakses via symbolic link `public/storage` jika diperlukan.
|
||||||
|
- Bagian ini biasanya dipakai untuk proposal, dokumentasi foto, dan file laporan yang diunduh kembali oleh pengguna.
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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": "^3.0.0",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
|
"vite": "^8.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?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="DB_URL" value=""/>
|
||||||
|
<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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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());
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
|
|
@ -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';
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
import './bootstrap';
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
window.axios = axios;
|
||||||
|
|
||||||
|
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', 'Detail Pertemuan ' . $pertemuan->pertemuan_ke . ' — ' . $organisasi->nama_organisasi)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<a href="{{ route('admin.absensi-ekskul.show', $organisasi) }}" class="btn btn-outline-secondary btn-sm">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i> Kembali
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Info Pertemuan --}}
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-info-circle me-2"></i>Pertemuan {{ $pertemuan->pertemuan_ke }}</h5>
|
||||||
|
<a href="{{ route('admin.absensi-ekskul.export-pertemuan-pdf', [$organisasi, $pertemuan]) }}" class="btn btn-danger btn-sm text-white">
|
||||||
|
<i class="fas fa-file-pdf me-1"></i> Export PDF
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-2 mb-3">
|
||||||
|
<label class="form-label fw-semibold text-muted small">Tanggal</label>
|
||||||
|
<p class="mb-0">{{ $pertemuan->tanggal->format('d F Y') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 mb-3">
|
||||||
|
<label class="form-label fw-semibold text-muted small">Semester</label>
|
||||||
|
<p class="mb-0">{{ $pertemuan->semester }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 mb-3">
|
||||||
|
<label class="form-label fw-semibold text-muted small">Tahun Ajaran</label>
|
||||||
|
<p class="mb-0">{{ $pertemuan->tahun_ajaran }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 mb-3">
|
||||||
|
<label class="form-label fw-semibold text-muted small">Pembina</label>
|
||||||
|
<p class="mb-0">{{ $pertemuan->pembina?->name ?? '-' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 mb-3">
|
||||||
|
<label class="form-label fw-semibold text-muted small">Organisasi</label>
|
||||||
|
<p class="mb-0">{{ $organisasi->nama_organisasi }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 mb-3">
|
||||||
|
<label class="form-label fw-semibold text-muted small">Dicatat pada</label>
|
||||||
|
<p class="mb-0">{{ $pertemuan->created_at->format('d M Y H:i') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if ($pertemuan->deskripsi_kegiatan)
|
||||||
|
<div class="mt-2">
|
||||||
|
<label class="form-label fw-semibold text-muted small">Deskripsi Kegiatan</label>
|
||||||
|
<p class="mb-0">{{ $pertemuan->deskripsi_kegiatan }}</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Foto Kegiatan --}}
|
||||||
|
@if ($pertemuan->fotoKegiatan->isNotEmpty())
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0"><i class="fas fa-images me-2"></i>Foto Kegiatan</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
@foreach ($pertemuan->fotoKegiatan as $foto)
|
||||||
|
<div class="col-md-4 col-sm-6">
|
||||||
|
<div class="card border">
|
||||||
|
<img src="{{ asset('storage/' . $foto->file_path) }}" class="card-img-top"
|
||||||
|
alt="Foto Kegiatan" style="height: 200px; object-fit: cover; cursor: pointer;"
|
||||||
|
data-bs-toggle="modal" data-bs-target="#fotoModal{{ $foto->id }}">
|
||||||
|
@if ($foto->keterangan)
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<small class="text-muted">{{ $foto->keterangan }}</small>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="fotoModal{{ $foto->id }}" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-body p-0">
|
||||||
|
<img src="{{ asset('storage/' . $foto->file_path) }}" class="w-100"
|
||||||
|
alt="Foto Kegiatan">
|
||||||
|
</div>
|
||||||
|
@if ($foto->keterangan)
|
||||||
|
<div class="modal-footer">
|
||||||
|
<span class="text-muted">{{ $foto->keterangan }}</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Daftar Absensi --}}
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0"><i class="fas fa-user-check me-2"></i>Daftar Absensi</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
@php
|
||||||
|
$hadir = $pertemuan->absensi->where('status', 'hadir')->count();
|
||||||
|
$izin = $pertemuan->absensi->where('status', 'izin')->count();
|
||||||
|
$sakit = $pertemuan->absensi->where('status', 'sakit')->count();
|
||||||
|
$alfa = $pertemuan->absensi->where('status', 'alfa')->count();
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="d-flex gap-3 mb-3 flex-wrap">
|
||||||
|
<span class="badge bg-success fs-6">Hadir: {{ $hadir }}</span>
|
||||||
|
<span class="badge bg-warning text-dark fs-6">Izin: {{ $izin }}</span>
|
||||||
|
<span class="badge bg-info fs-6">Sakit: {{ $sakit }}</span>
|
||||||
|
<span class="badge bg-danger fs-6">Alfa: {{ $alfa }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 50px;">No</th>
|
||||||
|
<th>Nama Lengkap</th>
|
||||||
|
<th>Kelas</th>
|
||||||
|
<th>No. HP</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse ($pertemuan->absensi->sortBy('anggota.nama') as $item)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $loop->iteration }}</td>
|
||||||
|
<td class="fw-semibold">{{ $item->anggota?->nama ?? '-' }}</td>
|
||||||
|
<td>{{ $item->anggota?->kelas ?? '-' }}</td>
|
||||||
|
<td>{{ $item->anggota?->no_hp ?? '-' }}</td>
|
||||||
|
<td>
|
||||||
|
@switch($item->status)
|
||||||
|
@case('hadir')
|
||||||
|
<span class="badge bg-success">Hadir</span>
|
||||||
|
@break
|
||||||
|
@case('izin')
|
||||||
|
<span class="badge bg-warning text-dark">Izin</span>
|
||||||
|
@break
|
||||||
|
@case('sakit')
|
||||||
|
<span class="badge bg-info">Sakit</span>
|
||||||
|
@break
|
||||||
|
@case('alfa')
|
||||||
|
<span class="badge bg-danger">Alfa</span>
|
||||||
|
@break
|
||||||
|
@endswitch
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center text-muted">Belum ada data absensi.</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', 'Absensi Ekstrakurikuler')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-clipboard-list me-2"></i>Monitoring Absensi Ekstrakurikuler</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{{-- Search --}}
|
||||||
|
<form method="GET" action="{{ route('admin.absensi-ekskul') }}" class="mb-3">
|
||||||
|
<div class="input-group" style="max-width: 400px;">
|
||||||
|
<input type="text" name="search" class="form-control form-control-sm"
|
||||||
|
placeholder="Cari organisasi..." value="{{ $search }}">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" type="submit">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</button>
|
||||||
|
@if ($search)
|
||||||
|
<a href="{{ route('admin.absensi-ekskul') }}" class="btn btn-outline-danger btn-sm">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 50px;">No</th>
|
||||||
|
<th>Nama Organisasi</th>
|
||||||
|
<th class="text-center">Jumlah Anggota</th>
|
||||||
|
<th class="text-center">Total Pertemuan</th>
|
||||||
|
<th style="width: 180px;">Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse ($organisasiList as $org)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $loop->iteration }}</td>
|
||||||
|
<td class="fw-semibold">{{ $org->nama_organisasi }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="badge bg-primary">{{ $org->anggota_count }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="badge bg-info">{{ $org->pertemuan_ekskul_count }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ route('admin.absensi-ekskul.show', $org) }}"
|
||||||
|
class="btn btn-info btn-sm">
|
||||||
|
<i class="fas fa-eye me-1"></i> Detail
|
||||||
|
</a>
|
||||||
|
@if ($org->pertemuan_ekskul_count > 0)
|
||||||
|
<a href="{{ route('admin.absensi-ekskul.export-pdf', $org) }}"
|
||||||
|
class="btn btn-danger btn-sm" title="Export PDF">
|
||||||
|
<i class="fas fa-file-pdf"></i>
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center text-muted py-4">
|
||||||
|
<i class="fas fa-building fa-2x mb-2 d-block opacity-50"></i>
|
||||||
|
Belum ada organisasi.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Rekap Kehadiran — {{ $organisasi->nama_organisasi }}</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Helvetica', 'Arial', sans-serif;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #4b5563;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
padding: 6px 8px;
|
||||||
|
vertical-align: top;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #f3f4f6;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-success {
|
||||||
|
background-color: #16a34a;
|
||||||
|
color: #fff;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-warning {
|
||||||
|
background-color: #d97706;
|
||||||
|
color: #fff;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-danger {
|
||||||
|
background-color: #dc2626;
|
||||||
|
color: #fff;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>Rekap Kehadiran Ekstrakurikuler</h2>
|
||||||
|
<h3>{{ $organisasi->nama_organisasi }}</h3>
|
||||||
|
|
||||||
|
<p class="meta">
|
||||||
|
@if (($exportType ?? 'semester') === 'bulan')
|
||||||
|
Bulan: {{ $namaBulan ?? '-' }} {{ $tahun ?? '-' }}
|
||||||
|
@else
|
||||||
|
Semester: {{ $semester ?? 'Semua Semester' }}
|
||||||
|
<br>
|
||||||
|
Tahun Ajaran: {{ $tahunAjaran ?? 'Semua Tahun Ajaran' }}
|
||||||
|
@endif
|
||||||
|
<br>
|
||||||
|
Total Pertemuan: {{ $totalPertemuan }}
|
||||||
|
<br>
|
||||||
|
Dicetak pada: {{ now()->format('d-m-Y H:i') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-center" style="width: 35px;">No</th>
|
||||||
|
<th>Nama Lengkap</th>
|
||||||
|
<th style="width: 70px;">Kelas</th>
|
||||||
|
<th style="width: 100px;">No. HP</th>
|
||||||
|
<th class="text-center" style="width: 50px;">Hadir</th>
|
||||||
|
<th class="text-center" style="width: 50px;">Izin</th>
|
||||||
|
<th class="text-center" style="width: 50px;">Sakit</th>
|
||||||
|
<th class="text-center" style="width: 50px;">Alfa</th>
|
||||||
|
<th class="text-center" style="width: 70px;">% Kehadiran</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse ($anggota as $member)
|
||||||
|
@php
|
||||||
|
$total = $member->hadir_count + $member->izin_count + $member->sakit_count + $member->alfa_count;
|
||||||
|
$persen = $total > 0 ? round(($member->hadir_count / $total) * 100, 1) : 0;
|
||||||
|
@endphp
|
||||||
|
<tr>
|
||||||
|
<td class="text-center">{{ $loop->iteration }}</td>
|
||||||
|
<td>{{ $member->nama }}</td>
|
||||||
|
<td>{{ $member->kelas }}</td>
|
||||||
|
<td>{{ $member->no_hp ?? '-' }}</td>
|
||||||
|
<td class="text-center">{{ $member->hadir_count }}</td>
|
||||||
|
<td class="text-center">{{ $member->izin_count }}</td>
|
||||||
|
<td class="text-center">{{ $member->sakit_count }}</td>
|
||||||
|
<td class="text-center">{{ $member->alfa_count }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
@if ($persen >= 75)
|
||||||
|
<span class="badge-success">{{ $persen }}%</span>
|
||||||
|
@elseif ($persen >= 50)
|
||||||
|
<span class="badge-warning">{{ $persen }}%</span>
|
||||||
|
@else
|
||||||
|
<span class="badge-danger">{{ $persen }}%</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="9" class="text-center">Belum ada data anggota.</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="summary">
|
||||||
|
@php
|
||||||
|
$totalHadir = $anggota->sum('hadir_count');
|
||||||
|
$totalIzin = $anggota->sum('izin_count');
|
||||||
|
$totalSakit = $anggota->sum('sakit_count');
|
||||||
|
$totalAlfa = $anggota->sum('alfa_count');
|
||||||
|
$grandTotal = $totalHadir + $totalIzin + $totalSakit + $totalAlfa;
|
||||||
|
$avgKehadiran = $grandTotal > 0 ? round(($totalHadir / $grandTotal) * 100, 1) : 0;
|
||||||
|
@endphp
|
||||||
|
<strong>Ringkasan:</strong>
|
||||||
|
Total Anggota: {{ $anggota->count() }} |
|
||||||
|
Rata-rata Kehadiran: {{ $avgKehadiran }}% |
|
||||||
|
Total Hadir: {{ $totalHadir }} |
|
||||||
|
Total Izin: {{ $totalIzin }} |
|
||||||
|
Total Sakit: {{ $totalSakit }} |
|
||||||
|
Total Alfa: {{ $totalAlfa }}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,348 @@
|
||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', 'Absensi — ' . $organisasi->nama_organisasi)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
@if (session('success'))
|
||||||
|
<div class="alert alert-success alert-dismissible fade show border-0 shadow-sm mb-3" role="alert">
|
||||||
|
<i class="fas fa-check-circle me-2"></i>{{ session('success') }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if (session('error'))
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show border-0 shadow-sm mb-3" role="alert">
|
||||||
|
<i class="fas fa-exclamation-circle me-2"></i>{{ session('error') }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<a href="{{ route('admin.absensi-ekskul') }}" class="btn btn-outline-secondary btn-sm">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i> Kembali ke Daftar Organisasi
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Filter Semester & Tahun Ajaran --}}
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3 align-items-end" id="adminFilterForm">
|
||||||
|
@if (count($semesterList) > 0 || count($tahunAjaranList) > 0)
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label fw-semibold">Semester</label>
|
||||||
|
<select id="filterSemester" class="form-select">
|
||||||
|
<option value="">Semua Semester</option>
|
||||||
|
@foreach ($semesterList as $sem)
|
||||||
|
<option value="{{ $sem }}" {{ $semester === $sem ? 'selected' : '' }}>{{ $sem }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label fw-semibold">Tahun Ajaran</label>
|
||||||
|
<select id="filterTahunAjaran" class="form-select">
|
||||||
|
<option value="">Semua Tahun Ajaran</option>
|
||||||
|
@foreach ($tahunAjaranList as $ta)
|
||||||
|
<option value="{{ $ta }}" {{ $tahunAjaran === $ta ? 'selected' : '' }}>
|
||||||
|
{{ $ta }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 d-flex gap-2">
|
||||||
|
<button type="button" class="btn btn-primary" id="btnApplyFilter">
|
||||||
|
<i class="fas fa-filter me-1"></i> Filter
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('admin.absensi-ekskul.show', $organisasi) }}"
|
||||||
|
class="btn btn-outline-secondary">
|
||||||
|
Reset
|
||||||
|
</a>
|
||||||
|
@if ($totalPertemuan > 0)
|
||||||
|
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#exportPdfModal">
|
||||||
|
<i class="fas fa-file-pdf me-1"></i> Export PDF
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="col text-muted py-2">
|
||||||
|
<i class="fas fa-info-circle me-1 text-info"></i> Belum ada data pertemuan untuk ekskul ini.
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($totalPertemuan > 0)
|
||||||
|
{{-- Rekap Kehadiran Anggota --}}
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="mb-0"><i class="fas fa-chart-bar me-2"></i>Rekap Kehadiran</h6>
|
||||||
|
<span class="badge bg-primary">{{ $totalPertemuan }} Pertemuan</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-bordered align-middle mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 50px;">No</th>
|
||||||
|
<th>Nama Lengkap</th>
|
||||||
|
<th>Kelas</th>
|
||||||
|
<th>No. HP</th>
|
||||||
|
<th class="text-center"><span class="badge bg-success">Hadir</span></th>
|
||||||
|
<th class="text-center"><span class="badge bg-warning text-dark">Izin</span></th>
|
||||||
|
<th class="text-center"><span class="badge bg-info">Sakit</span></th>
|
||||||
|
<th class="text-center"><span class="badge bg-danger">Alfa</span></th>
|
||||||
|
<th class="text-center">% Kehadiran</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse ($anggota as $member)
|
||||||
|
@php
|
||||||
|
$total = $member->hadir_count + $member->izin_count + $member->sakit_count + $member->alfa_count;
|
||||||
|
$persen = $total > 0 ? round(($member->hadir_count / $total) * 100, 1) : 0;
|
||||||
|
@endphp
|
||||||
|
<tr>
|
||||||
|
<td>{{ $loop->iteration }}</td>
|
||||||
|
<td class="fw-semibold">{{ $member->nama }}</td>
|
||||||
|
<td>{{ $member->kelas }}</td>
|
||||||
|
<td>{{ $member->no_hp ?? '-' }}</td>
|
||||||
|
<td class="text-center">{{ $member->hadir_count }}</td>
|
||||||
|
<td class="text-center">{{ $member->izin_count }}</td>
|
||||||
|
<td class="text-center">{{ $member->sakit_count }}</td>
|
||||||
|
<td class="text-center">{{ $member->alfa_count }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
@if ($persen >= 75)
|
||||||
|
<span class="badge bg-success">{{ $persen }}%</span>
|
||||||
|
@elseif ($persen >= 50)
|
||||||
|
<span class="badge bg-warning text-dark">{{ $persen }}%</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-danger">{{ $persen }}%</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="9" class="text-center text-muted py-4">
|
||||||
|
Belum ada data anggota.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Riwayat Pertemuan --}}
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0"><i class="fas fa-list me-2"></i>Riwayat Pertemuan</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Pertemuan</th>
|
||||||
|
<th>Tanggal</th>
|
||||||
|
<th>Semester</th>
|
||||||
|
<th>Tahun Ajaran</th>
|
||||||
|
<th class="text-center"><span class="badge bg-success">H</span></th>
|
||||||
|
<th class="text-center"><span class="badge bg-warning text-dark">I</span></th>
|
||||||
|
<th class="text-center"><span class="badge bg-info">S</span></th>
|
||||||
|
<th class="text-center"><span class="badge bg-danger">A</span></th>
|
||||||
|
<th style="width: 100px;">Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse ($pertemuan as $item)
|
||||||
|
<tr>
|
||||||
|
<td class="fw-semibold">Pertemuan {{ $item->pertemuan_ke }}</td>
|
||||||
|
<td>{{ $item->tanggal->format('d M Y') }}</td>
|
||||||
|
<td>{{ $item->semester }}</td>
|
||||||
|
<td>{{ $item->tahun_ajaran }}</td>
|
||||||
|
<td class="text-center">{{ $item->hadir_count }}</td>
|
||||||
|
<td class="text-center">{{ $item->izin_count }}</td>
|
||||||
|
<td class="text-center">{{ $item->sakit_count }}</td>
|
||||||
|
<td class="text-center">{{ $item->alfa_count }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex gap-1">
|
||||||
|
<a href="{{ route('admin.absensi-ekskul.detail', [$organisasi, $item]) }}"
|
||||||
|
class="btn btn-info btn-sm text-white" title="Detail">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('admin.absensi-ekskul.export-pertemuan-pdf', [$organisasi, $item]) }}"
|
||||||
|
class="btn btn-danger btn-sm text-white" title="Export PDF">
|
||||||
|
<i class="fas fa-file-pdf"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="9" class="text-center text-muted py-4">
|
||||||
|
Belum ada data pertemuan.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="card shadow-sm border-0 bg-light">
|
||||||
|
<div class="card-body text-center py-5">
|
||||||
|
<div class="mb-3">
|
||||||
|
<i class="fas fa-calendar-times text-warning fa-3x"></i>
|
||||||
|
</div>
|
||||||
|
<h5 class="text-secondary fw-semibold">Tidak Ada Data Kehadiran</h5>
|
||||||
|
<p class="text-muted mb-0">Tidak ditemukan data pertemuan atau absensi untuk ekskul ini pada kombinasi filter yang Anda pilih.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Modal Export PDF --}}
|
||||||
|
@if ($totalPertemuan > 0)
|
||||||
|
<div class="modal fade" id="exportPdfModal" tabindex="-1" aria-labelledby="exportPdfModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form action="{{ route('admin.absensi-ekskul.export-pdf', $organisasi) }}" method="GET" target="_blank">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="exportPdfModalLabel"><i class="fas fa-file-pdf text-danger me-2"></i>Pilihan Export PDF</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 class="form-label fw-semibold">Pilih Jenis Laporan</label>
|
||||||
|
<div class="d-flex gap-4">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="export_type" id="typeSemester" value="semester" checked>
|
||||||
|
<label class="form-check-label" for="typeSemester">
|
||||||
|
Per Semester
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="export_type" id="typeBulan" value="bulan">
|
||||||
|
<label class="form-check-label" for="typeBulan">
|
||||||
|
Per Bulan
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Per Semester Section -->
|
||||||
|
<div id="sectionSemester">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Semester</label>
|
||||||
|
<select name="semester" id="exportSemester" class="form-select">
|
||||||
|
<option value="">Semua Semester</option>
|
||||||
|
@foreach ($semesterList as $sem)
|
||||||
|
<option value="{{ $sem }}">{{ $sem }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Tahun Ajaran</label>
|
||||||
|
<select name="tahun_ajaran" id="exportTahunAjaran" class="form-select">
|
||||||
|
<option value="">Semua Tahun Ajaran</option>
|
||||||
|
@foreach ($tahunAjaranList as $ta)
|
||||||
|
<option value="{{ $ta }}">{{ $ta }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Per Bulan Section -->
|
||||||
|
<div id="sectionBulan" class="d-none">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Bulan</label>
|
||||||
|
<select name="bulan" id="exportBulan" class="form-select">
|
||||||
|
<option value="1">Januari</option>
|
||||||
|
<option value="2">Februari</option>
|
||||||
|
<option value="3">Maret</option>
|
||||||
|
<option value="4">April</option>
|
||||||
|
<option value="5">Mei</option>
|
||||||
|
<option value="6">Juni</option>
|
||||||
|
<option value="7">Juli</option>
|
||||||
|
<option value="8">Agustus</option>
|
||||||
|
<option value="9">September</option>
|
||||||
|
<option value="10">Oktober</option>
|
||||||
|
<option value="11">November</option>
|
||||||
|
<option value="12">Desember</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Tahun</label>
|
||||||
|
<select name="tahun" id="exportTahun" class="form-select">
|
||||||
|
@foreach ($tahunList as $th)
|
||||||
|
<option value="{{ $th }}">{{ $th }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
|
||||||
|
<button type="submit" class="btn btn-danger" data-bs-dismiss="modal">
|
||||||
|
<i class="fas fa-download me-1"></i> Unduh PDF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('scripts')
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const baseUrl = @json(route('admin.absensi-ekskul.show', $organisasi));
|
||||||
|
|
||||||
|
function getFilterParams() {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
const semester = document.getElementById('filterSemester')?.value || '';
|
||||||
|
const tahunAjaran = document.getElementById('filterTahunAjaran')?.value || '';
|
||||||
|
if (semester) params.set('semester', semester);
|
||||||
|
if (tahunAjaran) params.set('tahun_ajaran', tahunAjaran);
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFilter() {
|
||||||
|
const params = getFilterParams();
|
||||||
|
const queryStr = params.toString();
|
||||||
|
window.location.href = baseUrl + (queryStr ? '?' + queryStr : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter button
|
||||||
|
document.getElementById('btnApplyFilter')?.addEventListener('click', applyFilter);
|
||||||
|
|
||||||
|
// Auto-filter on dropdown change
|
||||||
|
document.getElementById('filterSemester')?.addEventListener('change', applyFilter);
|
||||||
|
document.getElementById('filterTahunAjaran')?.addEventListener('change', applyFilter);
|
||||||
|
|
||||||
|
// Modal radio toggle logic
|
||||||
|
const typeSemester = document.getElementById('typeSemester');
|
||||||
|
const typeBulan = document.getElementById('typeBulan');
|
||||||
|
const sectionSemester = document.getElementById('sectionSemester');
|
||||||
|
const sectionBulan = document.getElementById('sectionBulan');
|
||||||
|
|
||||||
|
if (typeSemester && typeBulan) {
|
||||||
|
typeSemester.addEventListener('change', function() {
|
||||||
|
if (this.checked) {
|
||||||
|
sectionSemester.classList.remove('d-none');
|
||||||
|
sectionBulan.classList.add('d-none');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
typeBulan.addEventListener('change', function() {
|
||||||
|
if (this.checked) {
|
||||||
|
sectionBulan.classList.remove('d-none');
|
||||||
|
sectionSemester.classList.add('d-none');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,571 @@
|
||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', 'Dashboard')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<!-- STAT CARDS -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<!-- Total Pengguna -->
|
||||||
|
<div class="col-md-6 col-lg-3">
|
||||||
|
<a href="{{ route('admin.users') }}" class="text-decoration-none">
|
||||||
|
<div class="card stat-card border-0 shadow-sm h-100 stat-card-link">
|
||||||
|
<div class="card-body d-flex flex-column justify-content-between">
|
||||||
|
<div class="d-flex align-items-start gap-2 mb-2">
|
||||||
|
<div class="stat-icon bg-primary-light">
|
||||||
|
<i class="fas fa-users text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<p class="text-muted small mb-1">Total Pengguna</p>
|
||||||
|
<div class="stat-value">
|
||||||
|
<h3 class="mb-0">{{ $stats['total_pengguna'] ?? 0 }}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Total Organisasi -->
|
||||||
|
<div class="col-md-6 col-lg-3">
|
||||||
|
<a href="{{ route('admin.organisasi') }}" class="text-decoration-none">
|
||||||
|
<div class="card stat-card border-0 shadow-sm h-100 stat-card-link">
|
||||||
|
<div class="card-body d-flex flex-column justify-content-between">
|
||||||
|
<div class="d-flex align-items-start gap-2 mb-2">
|
||||||
|
<div class="stat-icon bg-success-light">
|
||||||
|
<i class="fas fa-building text-success"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<p class="text-muted small mb-1">Total Organisasi</p>
|
||||||
|
<div class="stat-value">
|
||||||
|
<h3 class="mb-0">{{ $stats['total_organisasi'] ?? 0 }}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Total Kegiatan -->
|
||||||
|
<div class="col-md-6 col-lg-3">
|
||||||
|
<a href="{{ route('admin.kegiatan') }}" class="text-decoration-none">
|
||||||
|
<div class="card stat-card border-0 shadow-sm h-100 stat-card-link">
|
||||||
|
<div class="card-body d-flex flex-column justify-content-between">
|
||||||
|
<div class="d-flex align-items-start gap-2 mb-2">
|
||||||
|
<div class="stat-icon bg-purple-light">
|
||||||
|
<i class="fas fa-calendar-alt text-purple"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<p class="text-muted small mb-1">Total Kegiatan</p>
|
||||||
|
<div class="stat-value">
|
||||||
|
<h3 class="mb-0">{{ $stats['total_kegiatan'] ?? 0 }}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dokumentasi -->
|
||||||
|
<div class="col-md-6 col-lg-3">
|
||||||
|
<a href="{{ route('admin.dokumentasi') }}" class="text-decoration-none">
|
||||||
|
<div class="card stat-card border-0 shadow-sm h-100 stat-card-link">
|
||||||
|
<div class="card-body d-flex flex-column justify-content-between">
|
||||||
|
<div class="d-flex align-items-start gap-2 mb-2">
|
||||||
|
<div class="stat-icon bg-cyan-light">
|
||||||
|
<i class="fas fa-file-alt text-cyan"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<p class="text-muted small mb-1">Dokumentasi</p>
|
||||||
|
<div class="stat-value">
|
||||||
|
<h3 class="mb-0">{{ $stats['total_dokumentasi'] ?? 0 }}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<!-- Kegiatan Butuh Validasi -->
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<a href="{{ route('admin.validasi') }}" class="text-decoration-none">
|
||||||
|
<div class="card stat-card border-0 shadow-sm h-100 stat-card-link">
|
||||||
|
<div class="card-body d-flex flex-column justify-content-between">
|
||||||
|
<div class="d-flex align-items-start gap-2 mb-2">
|
||||||
|
<div class="stat-icon bg-secondary-light">
|
||||||
|
<i class="fas fa-hourglass-half text-secondary"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<p class="text-muted small mb-1">Kegiatan Butuh Validasi</p>
|
||||||
|
<div class="stat-value">
|
||||||
|
<h3 class="mb-0">{{ $stats['kegiatan_butuh_validasi'] ?? 0 }}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kegiatan Ditolak -->
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<a href="{{ route('admin.kegiatan') }}?status=ditolak" class="text-decoration-none">
|
||||||
|
<div class="card stat-card border-0 shadow-sm h-100 stat-card-link">
|
||||||
|
<div class="card-body d-flex flex-column justify-content-between">
|
||||||
|
<div class="d-flex align-items-start gap-2 mb-2">
|
||||||
|
<div class="stat-icon bg-danger-light">
|
||||||
|
<i class="fas fa-times-circle text-danger"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<p class="text-muted small mb-1">Kegiatan Ditolak</p>
|
||||||
|
<div class="stat-value">
|
||||||
|
<h3 class="mb-0">{{ $stats['kegiatan_ditolak'] ?? 0 }}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kegiatan Diterima -->
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<a href="{{ route('admin.kegiatan') }}?status=disetujui" class="text-decoration-none">
|
||||||
|
<div class="card stat-card border-0 shadow-sm h-100 stat-card-link">
|
||||||
|
<div class="card-body d-flex flex-column justify-content-between">
|
||||||
|
<div class="d-flex align-items-start gap-2 mb-2">
|
||||||
|
<div class="stat-icon bg-warning-light">
|
||||||
|
<i class="fas fa-check-circle text-warning"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<p class="text-muted small mb-1">Kegiatan Diterima</p>
|
||||||
|
<div class="stat-value">
|
||||||
|
<h3 class="mb-0">{{ $stats['kegiatan_disetujui'] ?? 0 }}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CHARTS SECTION -->
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<!-- Bar Chart -->
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="card-header bg-white border-0 d-flex justify-content-between align-items-center p-3">
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-0">Organisasi Paling Aktif (Top 10)</h6>
|
||||||
|
<small class="text-muted">Berdasarkan jumlah laporan kegiatan</small>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-sm btn-light" type="button" id="chartDropdown" data-bs-toggle="dropdown">
|
||||||
|
Tahun Ini <i class="fas fa-chevron-down ms-2"></i>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="chartDropdown">
|
||||||
|
<li><a class="dropdown-item"
|
||||||
|
href="{{ route('admin.dashboard', ['periode' => '6-bulan']) }}">6
|
||||||
|
Bulan</a></li>
|
||||||
|
<li><a class="dropdown-item"
|
||||||
|
href="{{ route('admin.dashboard', ['periode' => '12-bulan']) }}">12
|
||||||
|
Bulan</a></li>
|
||||||
|
<li><a class="dropdown-item"
|
||||||
|
href="{{ route('admin.dashboard', ['periode' => 'tahun-ini']) }}">Tahun Ini</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div style="position: relative; height: 300px;">
|
||||||
|
<canvas id="kegiatanChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pie Chart -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-header bg-white border-0 p-3">
|
||||||
|
<h6 class="mb-0">Ringkasan Kegiatan</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div style="position: relative; height: 300px; width: 100%;">
|
||||||
|
<canvas id="ringkasanChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KEGIATAN TERBARU & PERLU VALIDASI -->
|
||||||
|
<div class="row g-3">
|
||||||
|
<!-- Kegiatan Terbaru -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-header bg-white border-0 p-3 d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="mb-0">Kegiatan Terbaru</h6>
|
||||||
|
<a href="{{ route('admin.kegiatan') }}" class="text-primary small">Lihat Semua Kegiatan <i
|
||||||
|
class="fas fa-arrow-right ms-2"></i></a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
@forelse($kegiatanTerbaru as $item)
|
||||||
|
@php
|
||||||
|
$badgeClass = match ($item->status) {
|
||||||
|
'disetujui admin' => 'bg-success',
|
||||||
|
'disetujui pembina' => 'bg-warning text-dark',
|
||||||
|
'ditolak admin', 'ditolak pembina' => 'bg-danger',
|
||||||
|
'pending' => 'bg-secondary',
|
||||||
|
default => 'bg-light text-dark',
|
||||||
|
};
|
||||||
|
|
||||||
|
$statusLabel = match ($item->status) {
|
||||||
|
'disetujui admin' => 'Disetujui Admin',
|
||||||
|
'disetujui pembina' => 'Disetujui Pembina',
|
||||||
|
'ditolak admin' => 'Ditolak Admin',
|
||||||
|
'ditolak pembina' => 'Ditolak Pembina',
|
||||||
|
'pending' => 'Pending',
|
||||||
|
default => ucfirst($item->status),
|
||||||
|
};
|
||||||
|
@endphp
|
||||||
|
<div class="d-flex align-items-center mb-3 pb-3" style="border-bottom: 1px solid #e9ecef;">
|
||||||
|
<div class="me-3">
|
||||||
|
<i class="fas fa-calendar-alt fa-2x text-primary opacity-50"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="mb-1">{{ $item->nama_kegiatan }}</h6>
|
||||||
|
<small class="text-muted">{{ $item->organisasi->nama_organisasi ?? '-' }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="text-end ms-3" style="min-width: 95px;">
|
||||||
|
<span class="badge {{ $badgeClass }}">{{ $statusLabel }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-end ms-3" style="min-width: 120px;">
|
||||||
|
<small class="text-muted d-block"><i class="far fa-calendar"></i>
|
||||||
|
{{ $item->tanggal_mulai ? $item->tanggal_mulai->translatedFormat('d M Y') : '-' }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<p class="text-center text-muted">Belum ada data kegiatan.</p>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pengajuan Perlu Validasi -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-header bg-white border-0 p-3 d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="mb-0">Pengajuan Terbaru (Perlu Validasi)</h6>
|
||||||
|
<a href="{{ route('admin.validasi') }}" class="text-primary small">Lihat Semua Validasi <i
|
||||||
|
class="fas fa-arrow-right ms-2"></i></a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
@forelse($pengajuanTerbaru as $item)
|
||||||
|
@php
|
||||||
|
$badgeClass = match ($item->status) {
|
||||||
|
'disetujui admin' => 'bg-success',
|
||||||
|
'disetujui pembina' => 'bg-warning text-dark',
|
||||||
|
'ditolak admin', 'ditolak pembina' => 'bg-danger',
|
||||||
|
'pending' => 'bg-secondary',
|
||||||
|
default => 'bg-light text-dark',
|
||||||
|
};
|
||||||
|
|
||||||
|
$statusLabel = match ($item->status) {
|
||||||
|
'disetujui admin' => 'Disetujui Admin',
|
||||||
|
'disetujui pembina' => 'Disetujui Pembina',
|
||||||
|
'ditolak admin' => 'Ditolak Admin',
|
||||||
|
'ditolak pembina' => 'Ditolak Pembina',
|
||||||
|
'pending' => 'Pending',
|
||||||
|
default => ucfirst($item->status),
|
||||||
|
};
|
||||||
|
@endphp
|
||||||
|
<div class="d-flex align-items-center mb-3 pb-3" style="border-bottom: 1px solid #e9ecef;">
|
||||||
|
<div class="me-3">
|
||||||
|
<i class="fas fa-user-circle fa-2x text-primary opacity-50"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="mb-1">{{ $item->nama_kegiatan }}</h6>
|
||||||
|
<small class="text-muted">{{ $item->organisasi->nama_organisasi ?? '-' }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="text-end ms-3" style="min-width: 120px;">
|
||||||
|
<small class="text-muted d-block"><i class="far fa-calendar"></i>
|
||||||
|
{{ $item->tanggal_mulai ? $item->tanggal_mulai->translatedFormat('d M Y') : '-' }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="text-end ms-3" style="min-width: 95px;">
|
||||||
|
<span class="badge {{ $badgeClass }}">{{ $statusLabel }}</span>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('admin.validasi') }}" class="btn btn-sm btn-primary ms-3">Validasi</a>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<p class="text-center text-muted">Belum ada pengajuan dengan status Disetujui Pembina.</p>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('scripts')
|
||||||
|
@php
|
||||||
|
$defaultChartLabels = [
|
||||||
|
'PMR',
|
||||||
|
'OSIS',
|
||||||
|
'Pramuka',
|
||||||
|
'Rohis',
|
||||||
|
'KIR',
|
||||||
|
'Basket',
|
||||||
|
'Paskibra',
|
||||||
|
'Seni Musik',
|
||||||
|
'iKlim',
|
||||||
|
'Jurnalistik',
|
||||||
|
];
|
||||||
|
$chartLabelsSafe = !empty($chartLabels) ? $chartLabels : $defaultChartLabels;
|
||||||
|
$chartDataSafe = !empty($chartData) ? $chartData : [14, 11, 9, 7, 6, 5, 4, 3, 2, 1];
|
||||||
|
@endphp
|
||||||
|
<script>
|
||||||
|
// Bar Chart for Top 10 Organizations
|
||||||
|
const ctx = document.getElementById('kegiatanChart').getContext('2d');
|
||||||
|
const kegiatanChart = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: @json($chartLabelsSafe),
|
||||||
|
datasets: [{
|
||||||
|
label: 'Jumlah Laporan Kegiatan',
|
||||||
|
data: @json($chartDataSafe),
|
||||||
|
backgroundColor: '#0d6efd',
|
||||||
|
borderColor: '#0d6efd',
|
||||||
|
borderRadius: 6,
|
||||||
|
borderWidth: 0,
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
indexAxis: 'x',
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'top',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'category',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Organisasi'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
type: 'linear',
|
||||||
|
beginAtZero: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Jumlah Laporan Kegiatan'
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
stepSize: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pie Chart for Ringkasan
|
||||||
|
const ctxPie = document.getElementById('ringkasanChart').getContext('2d');
|
||||||
|
|
||||||
|
// Plugin to draw text in center of doughnut
|
||||||
|
const textCenterPlugin = {
|
||||||
|
id: 'textCenter',
|
||||||
|
afterDatasetsDraw(chart) {
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
ctx,
|
||||||
|
chartArea
|
||||||
|
} = chart;
|
||||||
|
const total = data.datasets[0].data.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
|
// Calculate center of the chart
|
||||||
|
const centerX = (chartArea.left + chartArea.right) / 2;
|
||||||
|
const centerY = (chartArea.top + chartArea.bottom) / 2;
|
||||||
|
|
||||||
|
// Draw total number
|
||||||
|
ctx.save();
|
||||||
|
ctx.font = 'bold 28px Arial';
|
||||||
|
ctx.fillStyle = '#333';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText(total, centerX, centerY - 15);
|
||||||
|
|
||||||
|
// Draw "Total" text
|
||||||
|
ctx.font = '14px Arial';
|
||||||
|
ctx.fillStyle = '#999';
|
||||||
|
ctx.fillText('Total', centerX, centerY + 15);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ringkasanChart = new Chart(ctxPie, {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels: ['Disetujui', 'Ditolak', 'Pending'],
|
||||||
|
datasets: [{
|
||||||
|
data: [
|
||||||
|
{{ $stats['kegiatan_disetujui'] ?? 0 }},
|
||||||
|
{{ $stats['kegiatan_ditolak'] ?? 0 }},
|
||||||
|
{{ $stats['kegiatan_pending'] ?? 0 }}
|
||||||
|
],
|
||||||
|
backgroundColor: [
|
||||||
|
'#28a745',
|
||||||
|
'#dc3545',
|
||||||
|
'#6c757d'
|
||||||
|
],
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 2
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'right',
|
||||||
|
labels: {
|
||||||
|
padding: 20,
|
||||||
|
boxWidth: 12,
|
||||||
|
font: {
|
||||||
|
size: 12
|
||||||
|
},
|
||||||
|
generateLabels: function(chart) {
|
||||||
|
const data = chart.data;
|
||||||
|
const datasets = data.datasets;
|
||||||
|
const total = datasets[0].data.reduce((a, b) => a + b, 0);
|
||||||
|
return data.labels.map((label, i) => {
|
||||||
|
const value = datasets[0].data[i];
|
||||||
|
const percentage = ((value / total) * 100).toFixed(0);
|
||||||
|
return {
|
||||||
|
text: `${label} ${value} (${percentage}%)`,
|
||||||
|
fillStyle: datasets[0].backgroundColor[i],
|
||||||
|
hidden: false,
|
||||||
|
index: i
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [textCenterPlugin]
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.stat-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-link {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-link:hover .stat-card {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-primary-light {
|
||||||
|
background-color: rgba(13, 110, 253, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-success-light {
|
||||||
|
background-color: rgba(40, 167, 69, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-warning-light {
|
||||||
|
background-color: rgba(255, 193, 7, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-purple-light {
|
||||||
|
background-color: rgba(124, 58, 237, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-info-light {
|
||||||
|
background-color: rgba(23, 162, 184, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-cyan-light {
|
||||||
|
background-color: rgba(6, 182, 212, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-danger-light {
|
||||||
|
background-color: rgba(220, 53, 69, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-secondary-light {
|
||||||
|
background-color: rgba(108, 117, 125, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
border-radius: 8px 8px 0 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-success {
|
||||||
|
color: #28a745 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-purple {
|
||||||
|
color: #7c3aed !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-cyan {
|
||||||
|
color: #06b6d4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .stat-value {
|
||||||
|
min-height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .stat-value h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue