Initial commit for combined ProjectKP
This commit is contained in:
@@ -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,docker-compose}.{yml,yaml}]
|
||||
indent_size = 4
|
||||
@@ -0,0 +1,65 @@
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
# APP_MAINTENANCE_STORE=database
|
||||
|
||||
# PHP_CLI_SERVER_WORKERS=4
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=laravel
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
# CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
@@ -0,0 +1,62 @@
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:kasmR3pne5hhhBbTf04a8QX7TORRbrCgIvkF4R/sSTU=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# 🔥 DATABASE (LARAGON MYSQL)
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=kp-asoy
|
||||
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
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
@@ -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,26 @@
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
/.codex
|
||||
/.cursor/
|
||||
/.idea
|
||||
/.nova
|
||||
/.phpunit.cache
|
||||
/.vscode
|
||||
/.zed
|
||||
/auth.json
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/vendor
|
||||
_ide_helper.php
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,2 @@
|
||||
ignore-scripts=true
|
||||
audit=true
|
||||
@@ -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,431 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ActivityController extends Controller
|
||||
{
|
||||
/**
|
||||
* ==========================================
|
||||
* AREA USER (PENGAWAS / PETUGAS)
|
||||
* ==========================================
|
||||
*/
|
||||
|
||||
private function apiUrl($endpoint = '')
|
||||
{
|
||||
return rtrim(env('BACKEND_API_URL'), '/') . '/' . ltrim($endpoint, '/');
|
||||
}
|
||||
|
||||
// 🔥 DASHBOARD USER
|
||||
public function index()
|
||||
{
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->get($this->apiUrl('/dashboard'));
|
||||
|
||||
$activities = collect([]);
|
||||
$stats = ['status_proses' => 0, 'status_selesai' => 0];
|
||||
|
||||
if ($response->successful()) {
|
||||
$data = $response->json();
|
||||
if (isset($data['data']['recent_activities'])) {
|
||||
// Mapping penugasan ke format yang diexpect view
|
||||
$activities = collect($data['data']['recent_activities'])->map(function($item) {
|
||||
$petugasNames = [];
|
||||
if(isset($item['petugas']) && is_array($item['petugas'])) {
|
||||
foreach($item['petugas'] as $p) {
|
||||
$petugasNames[] = $p['name'];
|
||||
}
|
||||
}
|
||||
$assignedTo = !empty($petugasNames) ? implode(', ', $petugasNames) : (session('user_name') ?? 'Pegawai');
|
||||
|
||||
return (object) [
|
||||
'id' => $item['id'],
|
||||
'nama_kegiatan' => $item['kegiatan'],
|
||||
'tanggal' => $item['tanggal_mulai'],
|
||||
'tanggal_selesai' => $item['tanggal_selesai'] ?? null,
|
||||
'status' => $item['status'] == 'proses' ? 'on_progres' : $item['status'],
|
||||
'lokasi' => $item['lokasi'] ?? null,
|
||||
'reviewed_at' => $item['reviewed_at'] ?? null,
|
||||
'user' => (object) ['name' => $assignedTo],
|
||||
];
|
||||
});
|
||||
}
|
||||
if (isset($data['data']['stats'])) {
|
||||
$stats = $data['data']['stats'];
|
||||
}
|
||||
}
|
||||
|
||||
$onProgres = $stats['status_proses'] ?? 0;
|
||||
$selesai = ($stats['status_selesai'] ?? 0) + ($stats['status_ditinjau'] ?? 0);
|
||||
|
||||
// Ambil notifikasi
|
||||
$notifResponse = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->get($this->apiUrl('/notifications'));
|
||||
$notifications = [];
|
||||
if ($notifResponse->successful()) {
|
||||
$notifications = $notifResponse->json()['data'] ?? [];
|
||||
}
|
||||
|
||||
$unreadCount = collect($notifications)->where('read_at', null)->count();
|
||||
|
||||
return view('user.dashboard', compact(
|
||||
'activities',
|
||||
'onProgres',
|
||||
'selesai',
|
||||
'notifications',
|
||||
'unreadCount'
|
||||
));
|
||||
}
|
||||
|
||||
// 🔥 LIST SEMUA KEGIATAN USER
|
||||
public function list()
|
||||
{
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->get($this->apiUrl('/penugasan'));
|
||||
|
||||
$activities = collect([]);
|
||||
if ($response->successful()) {
|
||||
$data = $response->json();
|
||||
$activities = collect($data['data'])->map(function($item) {
|
||||
$petugasNames = [];
|
||||
if(isset($item['petugas']) && is_array($item['petugas'])) {
|
||||
foreach($item['petugas'] as $p) {
|
||||
$petugasNames[] = $p['name'];
|
||||
}
|
||||
}
|
||||
$assignedTo = !empty($petugasNames) ? implode(', ', $petugasNames) : (session('user_name') ?? 'Pegawai');
|
||||
|
||||
return (object) [
|
||||
'id' => $item['id'],
|
||||
'nama_kegiatan' => $item['kegiatan'],
|
||||
'tanggal' => $item['tanggal_mulai'],
|
||||
'tanggal_selesai' => $item['tanggal_selesai'] ?? null,
|
||||
'lokasi' => $item['lokasi'] ?? null,
|
||||
'status' => $item['status'] == 'proses' ? 'on_progres' : $item['status'],
|
||||
'user' => (object) ['name' => $assignedTo],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
return view('user.activity-list', compact('activities'));
|
||||
}
|
||||
|
||||
// 🔥 SIMPAN ACTIVITY BARU
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'nama_kegiatan' => 'required|string|max:255',
|
||||
'tanggal' => 'required|date',
|
||||
'lokasi' => 'nullable|string|max:255',
|
||||
'deskripsi' => 'nullable|string',
|
||||
'image' => 'nullable|mimes:jpeg,png,jpg,pdf|max:2048',
|
||||
]);
|
||||
|
||||
$payload = [
|
||||
'nomor_surat' => null, // Tidak perlu generate karena ini laporan pegawai
|
||||
'kegiatan' => $request->nama_kegiatan,
|
||||
'tanggal_mulai' => $request->tanggal,
|
||||
'lokasi' => $request->lokasi,
|
||||
'keterangan' => $request->deskripsi,
|
||||
'status_note' => $request->status_note,
|
||||
];
|
||||
|
||||
$http = \Illuminate\Support\Facades\Http::withToken(session('api_token'));
|
||||
|
||||
if ($request->hasFile('image')) {
|
||||
$file = $request->file('image');
|
||||
$http = $http->attach('lampiran', file_get_contents($file), $file->getClientOriginalName());
|
||||
$payload['pegawai_ids[0]'] = session('user_id');
|
||||
} else {
|
||||
$payload['pegawai_ids'] = [session('user_id')];
|
||||
}
|
||||
|
||||
$response = $http->post($this->apiUrl('/penugasan'), $payload);
|
||||
|
||||
if ($response->successful()) {
|
||||
return redirect()->route('activity.list')->with('success', 'Kegiatan berhasil ditambahkan');
|
||||
}
|
||||
|
||||
return back()->with('error', 'Gagal menambahkan kegiatan: ' . $response->json('message', 'Error API'));
|
||||
}
|
||||
|
||||
// 🔥 DETAIL ACTIVITY
|
||||
public function show($id)
|
||||
{
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->get($this->apiUrl('/penugasan/' . $id));
|
||||
|
||||
if (!$response->successful()) {
|
||||
return redirect()->route('activity.list')->with('error', 'Data tidak ditemukan');
|
||||
}
|
||||
|
||||
$data = $response->json()['data'];
|
||||
$activity = (object) [
|
||||
'id' => $data['id'],
|
||||
'nama_kegiatan' => $data['kegiatan'],
|
||||
'tanggal' => $data['tanggal_mulai'],
|
||||
'tanggal_selesai' => $data['tanggal_selesai'] ?? null,
|
||||
'lokasi' => $data['lokasi'] ?? '-',
|
||||
'deskripsi' => $data['keterangan'] ?? '-',
|
||||
'status_note' => $data['status_note'] ?? '-',
|
||||
'lampiran' => $data['lampiran'] ?? null,
|
||||
'updated_at' => $data['updated_at'] ?? now(),
|
||||
'status' => $data['status'] == 'proses' ? 'on_progres' : $data['status'],
|
||||
'user' => (object) ['name' => $data['pembuat']['name'] ?? session('user_name')],
|
||||
'progress' => $data['progress'] ?? [],
|
||||
'petugas' => $data['petugas'] ?? []
|
||||
];
|
||||
|
||||
return view('user.activity-detail', compact('activity'));
|
||||
}
|
||||
|
||||
// 🔥 EDIT ACTIVITY
|
||||
public function edit($id)
|
||||
{
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->get($this->apiUrl('/penugasan/' . $id));
|
||||
|
||||
if (!$response->successful()) {
|
||||
return redirect()->route('activity.list')->with('error', 'Data tidak ditemukan');
|
||||
}
|
||||
|
||||
$data = $response->json()['data'];
|
||||
|
||||
$isPetugas = false;
|
||||
if(isset($data['petugas'])) {
|
||||
foreach($data['petugas'] as $p) {
|
||||
if($p['id'] == session('user_id')) {
|
||||
$isPetugas = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isPetugas) {
|
||||
return redirect()->route('activity.list')->with('error', 'Anda tidak memiliki hak untuk mengedit laporan ini.');
|
||||
}
|
||||
|
||||
$activity = (object) [
|
||||
'id' => $data['id'],
|
||||
'nama_kegiatan' => $data['kegiatan'],
|
||||
'tanggal' => $data['tanggal_mulai'],
|
||||
'lokasi' => $data['lokasi'] ?? '',
|
||||
'deskripsi' => $data['keterangan'] ?? '',
|
||||
'status_note' => $data['status_note'] ?? '',
|
||||
];
|
||||
|
||||
return view('user.activity-edit', compact('activity'));
|
||||
}
|
||||
|
||||
// 🔥 UPDATE ACTIVITY
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))->get($this->apiUrl('/penugasan/' . $id));
|
||||
if ($response->successful()) {
|
||||
$data = $response->json()['data'];
|
||||
$isPetugas = false;
|
||||
if(isset($data['petugas'])) {
|
||||
foreach($data['petugas'] as $p) {
|
||||
if($p['id'] == session('user_id')) {
|
||||
$isPetugas = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$isPetugas) {
|
||||
return redirect()->route('activity.list')->with('error', 'Anda tidak memiliki hak untuk mengedit laporan ini.');
|
||||
}
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'nama_kegiatan' => 'required|string|max:255',
|
||||
'tanggal' => 'required|date',
|
||||
'lokasi' => 'nullable|string|max:255',
|
||||
'deskripsi' => 'nullable|string',
|
||||
'image' => 'nullable|mimes:jpeg,png,jpg,pdf|max:2048',
|
||||
]);
|
||||
|
||||
$payload = [
|
||||
'nomor_surat' => null, // Tidak perlu generate
|
||||
'kegiatan' => $request->nama_kegiatan,
|
||||
'tanggal_mulai' => $request->tanggal,
|
||||
'lokasi' => $request->lokasi,
|
||||
'keterangan' => $request->deskripsi,
|
||||
'status_note' => $request->status_note,
|
||||
// Laravel HTTP client sometimes fails PUT with multipart, we can simulate it with POST + _method=PUT
|
||||
'_method' => 'PUT',
|
||||
];
|
||||
|
||||
$http = \Illuminate\Support\Facades\Http::withToken(session('api_token'));
|
||||
|
||||
if ($request->hasFile('image')) {
|
||||
$file = $request->file('image');
|
||||
$http = $http->attach('lampiran', file_get_contents($file), $file->getClientOriginalName());
|
||||
$payload['pegawai_ids[0]'] = session('user_id');
|
||||
} else {
|
||||
$payload['pegawai_ids'] = [session('user_id')];
|
||||
}
|
||||
|
||||
// Post with _method=PUT to handle multipart update safely
|
||||
$response = $http->post($this->apiUrl('/penugasan/' . $id), $payload);
|
||||
|
||||
if ($response->successful()) {
|
||||
return redirect()->route('activity.detail', $id)
|
||||
->with('success', 'Data berhasil diperbarui');
|
||||
}
|
||||
|
||||
return back()->with('error', 'Gagal memperbarui: ' . $response->json('message'));
|
||||
}
|
||||
|
||||
// 🔥 SELESAIKAN ACTIVITY
|
||||
public function selesai($id)
|
||||
{
|
||||
$responseCheck = \Illuminate\Support\Facades\Http::withToken(session('api_token'))->get($this->apiUrl('/penugasan/' . $id));
|
||||
if ($responseCheck->successful()) {
|
||||
$data = $responseCheck->json()['data'];
|
||||
$isPetugas = false;
|
||||
if(isset($data['petugas'])) {
|
||||
foreach($data['petugas'] as $p) {
|
||||
if($p['id'] == session('user_id')) {
|
||||
$isPetugas = true; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$isPetugas) {
|
||||
return redirect()->route('activity.list')->with('error', 'Anda tidak memiliki hak untuk menyelesaikan laporan ini.');
|
||||
}
|
||||
}
|
||||
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->post($this->apiUrl("/penugasan/{$id}/update-status"), [
|
||||
'status' => 'selesai'
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
return back()->with('success', 'Kegiatan telah ditandai sebagai Selesai');
|
||||
}
|
||||
|
||||
return back()->with('error', 'Gagal menandai selesai');
|
||||
}
|
||||
|
||||
public function storeProgress(Request $request, $id)
|
||||
{
|
||||
$responseCheck = \Illuminate\Support\Facades\Http::withToken(session('api_token'))->get($this->apiUrl('/penugasan/' . $id));
|
||||
if ($responseCheck->successful()) {
|
||||
$data = $responseCheck->json()['data'];
|
||||
$isPetugas = false;
|
||||
if(isset($data['petugas'])) {
|
||||
foreach($data['petugas'] as $p) {
|
||||
if($p['id'] == session('user_id')) {
|
||||
$isPetugas = true; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$isPetugas) {
|
||||
return redirect()->route('activity.list')->with('error', 'Anda tidak memiliki hak untuk menambahkan progress pada laporan ini.');
|
||||
}
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'note' => 'required|string',
|
||||
'attachment' => 'nullable|file|mimes:jpg,jpeg,png,pdf|max:2048'
|
||||
]);
|
||||
|
||||
$http = \Illuminate\Support\Facades\Http::withToken(session('api_token'));
|
||||
|
||||
if ($request->hasFile('attachment')) {
|
||||
$file = $request->file('attachment');
|
||||
$http = $http->attach(
|
||||
'attachment',
|
||||
file_get_contents($file->getPathname()),
|
||||
$file->getClientOriginalName()
|
||||
);
|
||||
}
|
||||
|
||||
$response = $http->post($this->apiUrl("/penugasan/{$id}/progress"), [
|
||||
'note' => $request->note,
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
return back()->with('success', 'Progres / Log pembaruan berhasil ditambahkan.');
|
||||
}
|
||||
|
||||
return back()->with('error', 'Gagal menambahkan progres: ' . $response->json('message', 'Error'));
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================
|
||||
* AREA ADMIN (KASI / KABID)
|
||||
* ==========================================
|
||||
*/
|
||||
|
||||
public function adminDashboard()
|
||||
{
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->acceptJson()
|
||||
->get($this->apiUrl('/dashboard'));
|
||||
|
||||
$stats = ['total' => 0, 'selesai' => 0, 'proses' => 0, 'users' => 0];
|
||||
$activities = collect([]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$data = $response->json();
|
||||
if (isset($data['data']['stats'])) {
|
||||
$st = $data['data']['stats'];
|
||||
$stats['total'] = $st['total_tugas'] ?? 0;
|
||||
$stats['selesai'] = ($st['status_selesai'] ?? 0) + ($st['status_ditinjau'] ?? 0);
|
||||
$stats['proses'] = $st['status_proses'] ?? 0;
|
||||
}
|
||||
if (isset($data['data']['recent_activities'])) {
|
||||
$activities = collect($data['data']['recent_activities'])->map(function($item) {
|
||||
$petugasNames = 'System';
|
||||
if (!empty($item['petugas']) && is_array($item['petugas'])) {
|
||||
$names = [];
|
||||
foreach ($item['petugas'] as $p) {
|
||||
if (isset($p['username'])) {
|
||||
$names[] = $p['username'];
|
||||
} elseif (isset($p['name'])) {
|
||||
$names[] = $p['name'];
|
||||
}
|
||||
}
|
||||
if (!empty($names)) {
|
||||
$petugasNames = implode(', ', $names);
|
||||
}
|
||||
}
|
||||
|
||||
return (object) [
|
||||
'id' => $item['id'],
|
||||
'nama_kegiatan' => $item['kegiatan'],
|
||||
'tanggal' => $item['tanggal_mulai'],
|
||||
'tanggal_selesai' => $item['tanggal_selesai'] ?? null,
|
||||
'status' => $item['status'] == 'proses' ? 'on_progres' : $item['status'],
|
||||
'user' => (object) ['name' => $petugasNames],
|
||||
'keterangan' => $item['keterangan'] ?? $item['kegiatan'] ?? '-',
|
||||
'status_note' => $item['status_note'] ?? null,
|
||||
'lokasi' => $item['lokasi'] ?? '-',
|
||||
'lampiran' => $item['lampiran'] ?? null
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Mock data karena backend belum support
|
||||
$topUsers = [];
|
||||
$weeklyData = [0, 0, 0, 0, 0, 0, 0];
|
||||
$divisionProgress = [
|
||||
['name' => 'Operasional', 'value' => 75, 'color' => '#378ADD'],
|
||||
['name' => 'IT', 'value' => 45, 'color' => '#E24B4A'],
|
||||
['name' => 'SDM', 'value' => 90, 'color' => '#EF9F27'],
|
||||
];
|
||||
|
||||
return view('admin.dashboard', compact(
|
||||
'activities',
|
||||
'stats',
|
||||
'weeklyData',
|
||||
'topUsers',
|
||||
'divisionProgress'
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class ActivityController extends Controller
|
||||
{
|
||||
private function apiUrl($endpoint = '')
|
||||
{
|
||||
return rtrim(env('BACKEND_API_URL'), '/') . '/' . ltrim($endpoint, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* FUNGSI DASHBOARD ADMIN
|
||||
*/
|
||||
public function dashboard()
|
||||
{
|
||||
$response = Http::withToken(session('api_token'))
|
||||
->acceptJson()
|
||||
->get($this->apiUrl('/dashboard'));
|
||||
|
||||
$stats = ['total' => 0, 'selesai' => 0, 'proses' => 0, 'users' => 0];
|
||||
$activities = [];
|
||||
|
||||
if ($response->successful()) {
|
||||
$data = $response->json();
|
||||
if (isset($data['data']['stats'])) {
|
||||
$st = $data['data']['stats'];
|
||||
$stats['total'] = $st['total_tugas'] ?? 0;
|
||||
$stats['selesai'] = $st['status_selesai'] ?? 0;
|
||||
$stats['proses'] = $st['status_proses'] ?? 0;
|
||||
}
|
||||
if (isset($data['data']['recent_activities'])) {
|
||||
$activities = collect($data['data']['recent_activities'])->map(function($item) {
|
||||
$petugasNames = 'System';
|
||||
if (!empty($item['petugas']) && is_array($item['petugas'])) {
|
||||
$names = [];
|
||||
foreach ($item['petugas'] as $p) {
|
||||
if (isset($p['username'])) {
|
||||
$names[] = $p['username'];
|
||||
} elseif (isset($p['name'])) {
|
||||
$names[] = $p['name'];
|
||||
}
|
||||
}
|
||||
if (!empty($names)) {
|
||||
$petugasNames = implode(', ', $names);
|
||||
}
|
||||
}
|
||||
|
||||
return (object) [
|
||||
'id' => $item['id'],
|
||||
'nama_kegiatan' => $item['kegiatan'],
|
||||
'tanggal' => $item['tanggal_mulai'],
|
||||
'status' => $item['status'] == 'proses' ? 'on_progres' : $item['status'],
|
||||
'user' => (object) ['name' => $petugasNames],
|
||||
'keterangan' => $item['keterangan'] ?? $item['kegiatan'] ?? '-',
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$topUsers = [];
|
||||
$weeklyData = [0, 0, 0, 0, 0, 0, 0];
|
||||
$divisionProgress = [
|
||||
['name' => 'Operasional', 'pct' => 75, 'clr' => 'linear-gradient(90deg,#3b82f6,#6366f1)'],
|
||||
['name' => 'IT', 'pct' => 45, 'clr' => 'linear-gradient(90deg,#a855f7,#ec4899)'],
|
||||
['name' => 'SDM', 'pct' => 90, 'clr' => 'linear-gradient(90deg,#f97316,#f59e0b)'],
|
||||
['name' => 'Keuangan', 'pct' => 64, 'clr' => 'linear-gradient(90deg,#22c55e,#10b981)'],
|
||||
];
|
||||
|
||||
return view('admin.dashboard', compact(
|
||||
'stats',
|
||||
'activities',
|
||||
'topUsers',
|
||||
'weeklyData',
|
||||
'divisionProgress'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* HALAMAN INDEX (DAFTAR KEGIATAN DENGAN FILTER)
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$response = Http::withToken(session('api_token'))
|
||||
->acceptJson()
|
||||
->get($this->apiUrl('/penugasan'));
|
||||
|
||||
$items = collect([]);
|
||||
if ($response->successful()) {
|
||||
$data = $response->json();
|
||||
$items = collect($data['data'])->map(function($item) {
|
||||
$petugasNames = 'System';
|
||||
if (!empty($item['petugas']) && is_array($item['petugas'])) {
|
||||
$names = [];
|
||||
foreach ($item['petugas'] as $p) {
|
||||
if (isset($p['username'])) {
|
||||
$names[] = $p['username'];
|
||||
} elseif (isset($p['name'])) {
|
||||
$names[] = $p['name'];
|
||||
}
|
||||
}
|
||||
if (!empty($names)) {
|
||||
$petugasNames = implode(', ', $names);
|
||||
}
|
||||
}
|
||||
|
||||
return (object) [
|
||||
'id' => $item['id'],
|
||||
'nama_kegiatan' => $item['kegiatan'],
|
||||
'tanggal' => $item['tanggal_mulai'],
|
||||
'status' => $item['status'] == 'proses' ? 'on_progres' : $item['status'],
|
||||
'user' => (object) ['name' => $petugasNames],
|
||||
];
|
||||
});
|
||||
|
||||
// Sembunyikan kegiatan yang sudah dibatalkan
|
||||
$items = $items->filter(function($item) {
|
||||
return $item->status !== 'dibatalkan';
|
||||
})->values();
|
||||
}
|
||||
|
||||
// Simpan semua item sebelum filter untuk statistik
|
||||
$allItems = $items;
|
||||
|
||||
// FILTER SEARCH
|
||||
if ($request->search) {
|
||||
$search = strtolower($request->search);
|
||||
$items = $items->filter(function($item) use ($search) {
|
||||
return str_contains(strtolower($item->nama_kegiatan), $search) ||
|
||||
str_contains(strtolower($item->user->name), $search);
|
||||
});
|
||||
}
|
||||
|
||||
// FILTER STATUS
|
||||
if ($request->status && $request->status != 'semua') {
|
||||
$items = $items->filter(function($item) use ($request) {
|
||||
return $item->status === $request->status;
|
||||
});
|
||||
}
|
||||
|
||||
// Manual Pagination
|
||||
$perPage = 8;
|
||||
$currentPage = \Illuminate\Pagination\Paginator::resolveCurrentPage();
|
||||
$currentItems = $items->slice(($currentPage - 1) * $perPage, $perPage)->all();
|
||||
$activities = new \Illuminate\Pagination\LengthAwarePaginator($currentItems, count($items), $perPage, $currentPage, [
|
||||
'path' => \Illuminate\Pagination\Paginator::resolveCurrentPath(),
|
||||
'query' => $request->query(),
|
||||
]);
|
||||
|
||||
$divisis = collect(['-']);
|
||||
|
||||
return view('admin.kegiatan.index', compact('activities', 'divisis', 'allItems'));
|
||||
}
|
||||
|
||||
// DETAIL
|
||||
public function show($id)
|
||||
{
|
||||
$response = Http::withToken(session('api_token'))
|
||||
->acceptJson()
|
||||
->get($this->apiUrl('/penugasan/' . $id));
|
||||
|
||||
if (!$response->successful()) {
|
||||
return redirect()->route('admin.kegiatan.index')->with('error', 'Data tidak ditemukan');
|
||||
}
|
||||
|
||||
$data = $response->json()['data'];
|
||||
$petugas = collect($data['petugas'] ?? []);
|
||||
|
||||
$petugasNames = 'System';
|
||||
if ($petugas->isNotEmpty()) {
|
||||
$names = [];
|
||||
foreach ($petugas as $p) {
|
||||
if (isset($p['username'])) {
|
||||
$names[] = $p['username'];
|
||||
} elseif (isset($p['name'])) {
|
||||
$names[] = $p['name'];
|
||||
}
|
||||
}
|
||||
if (!empty($names)) {
|
||||
$petugasNames = implode(', ', $names);
|
||||
}
|
||||
}
|
||||
|
||||
$activity = (object) [
|
||||
'id' => $data['id'],
|
||||
'nomor_surat' => $data['nomor_surat'] ?? '-',
|
||||
'nama_kegiatan' => $data['kegiatan'],
|
||||
'tanggal' => $data['tanggal_mulai'],
|
||||
'tanggal_selesai' => $data['tanggal_selesai'] ?? null,
|
||||
'lokasi' => '-',
|
||||
'deskripsi' => '-',
|
||||
'updated_at' => $data['updated_at'] ?? now(),
|
||||
'status' => $data['status'] == 'proses' ? 'on_progres' : $data['status'],
|
||||
'user' => (object) ['name' => $petugasNames],
|
||||
'petugas' => $petugas,
|
||||
];
|
||||
|
||||
return view('admin.kegiatan.detail', compact('activity'));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,555 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonPeriod;
|
||||
|
||||
class SuperAdminController extends Controller
|
||||
{
|
||||
private function apiUrl($endpoint = '')
|
||||
{
|
||||
return rtrim(env('BACKEND_API_URL'), '/') . '/' . ltrim($endpoint, '/');
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->get($this->apiUrl('/penugasan'), ['is_archived' => 'false']);
|
||||
|
||||
$dashResponse = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->get($this->apiUrl('/dashboard'));
|
||||
|
||||
$items = collect([]);
|
||||
if ($response->successful()) {
|
||||
$items = collect($response->json()['data'])->map(function($item) {
|
||||
return (object) [
|
||||
'id' => $item['id'],
|
||||
'nama_kegiatan' => $item['kegiatan'],
|
||||
'tanggal' => $item['tanggal_mulai'],
|
||||
'tanggal_selesai' => $item['tanggal_selesai'] ?? null,
|
||||
'created_at' => \Carbon\Carbon::parse($item['created_at']),
|
||||
'status' => $item['status'] == 'proses' ? 'on_progres' : $item['status'],
|
||||
'user' => (object) ['name' => $item['pembuat']['name'] ?? 'System'],
|
||||
'petugas' => $item['petugas'] ?? [],
|
||||
'pembuat' => $item['pembuat'] ?? []
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
$allItems = clone $items;
|
||||
$items = $items->filter(function($item) {
|
||||
return $item->status !== 'dibatalkan';
|
||||
})->values();
|
||||
|
||||
// Manual Pagination
|
||||
$perPage = 10;
|
||||
$currentPage = \Illuminate\Pagination\Paginator::resolveCurrentPage();
|
||||
$currentItems = $items->slice(($currentPage - 1) * $perPage, $perPage)->all();
|
||||
$activities = new \Illuminate\Pagination\LengthAwarePaginator($currentItems, count($items), $perPage, $currentPage, [
|
||||
'path' => \Illuminate\Pagination\Paginator::resolveCurrentPath()
|
||||
]);
|
||||
|
||||
$stats = [
|
||||
'total' => $items->count(),
|
||||
'users' => 0, // diisi nanti
|
||||
'proses' => $items->where('status', 'on_progres')->count(),
|
||||
'deleted' => $allItems->where('status', 'dibatalkan')->count(),
|
||||
];
|
||||
|
||||
// Ambil User
|
||||
$usersResponse = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->get($this->apiUrl('/users'));
|
||||
$users = collect([]);
|
||||
if ($usersResponse->successful()) {
|
||||
$users = collect($usersResponse->json()['data']);
|
||||
$stats['users'] = $users->count();
|
||||
}
|
||||
|
||||
// ── Top 5 Petugas (Dihitung dari penugasan)
|
||||
$topUsers = $users->map(function($u) use ($items) {
|
||||
$count = $items->filter(function($item) use ($u) {
|
||||
if (isset($item->petugas) && is_array($item->petugas)) {
|
||||
foreach ($item->petugas as $p) {
|
||||
if (isset($p['id']) && $p['id'] == $u['id']) return true;
|
||||
}
|
||||
}
|
||||
return isset($item->pembuat['id']) && $item->pembuat['id'] == $u['id'];
|
||||
})->count();
|
||||
|
||||
return (object) [
|
||||
'name' => $u['name'] ?? 'System',
|
||||
'activities_count' => $count
|
||||
];
|
||||
})->sortByDesc('activities_count')->take(5)->values();
|
||||
|
||||
// ── Data Grafik
|
||||
$chartData = $this->buildChartData($items);
|
||||
|
||||
$system = [
|
||||
'server' => rand(25, 55),
|
||||
'database' => rand(45, 75),
|
||||
'storage' => rand(60, 85),
|
||||
];
|
||||
|
||||
return view('superadmin.dashboard', compact(
|
||||
'activities',
|
||||
'stats',
|
||||
'topUsers',
|
||||
'chartData',
|
||||
'system'
|
||||
));
|
||||
}
|
||||
|
||||
private function buildChartData($items)
|
||||
{
|
||||
$now = Carbon::now();
|
||||
|
||||
// ── MINGGU INI ─────────────────────────────────
|
||||
$startOfWeek = $now->copy()->startOfWeek(Carbon::MONDAY);
|
||||
$endOfWeek = $now->copy()->endOfWeek(Carbon::SUNDAY);
|
||||
|
||||
$weekLabels = ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min'];
|
||||
$weekValues = array_fill(0, 7, 0);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$date = $item->created_at;
|
||||
if ($date->between($startOfWeek, $endOfWeek)) {
|
||||
$idx = $date->dayOfWeekIso - 1;
|
||||
$weekValues[$idx]++;
|
||||
}
|
||||
}
|
||||
|
||||
// ── BULAN INI ──────────────────────────────
|
||||
$startOfMonth = $now->copy()->startOfMonth();
|
||||
$endOfMonth = $now->copy()->endOfMonth();
|
||||
$daysInMonth = $now->daysInMonth;
|
||||
|
||||
$monthLabels = [];
|
||||
$monthValues = array_fill(0, $daysInMonth, 0);
|
||||
|
||||
for ($i = 1; $i <= $daysInMonth; $i++) {
|
||||
$monthLabels[] = ($i % 5 === 1 || $i === 1) ? (string) $i : '';
|
||||
}
|
||||
|
||||
foreach ($items as $item) {
|
||||
$date = $item->created_at;
|
||||
if ($date->between($startOfMonth, $endOfMonth)) {
|
||||
$monthValues[$date->day - 1]++;
|
||||
}
|
||||
}
|
||||
|
||||
// ── TAHUN INI ────────────────────────────────
|
||||
$monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Ags', 'Sep', 'Okt', 'Nov', 'Des'];
|
||||
$yearValues = array_fill(0, 12, 0);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$date = $item->created_at;
|
||||
if ($date->year === $now->year) {
|
||||
$yearValues[$date->month - 1]++;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'week' => [
|
||||
'labels' => $weekLabels,
|
||||
'values' => $weekValues,
|
||||
'todayIndex' => $now->dayOfWeekIso - 1,
|
||||
],
|
||||
'month' => [
|
||||
'labels' => $monthLabels,
|
||||
'values' => $monthValues,
|
||||
'todayIndex' => $now->day - 1,
|
||||
],
|
||||
'year' => [
|
||||
'labels' => $monthNames,
|
||||
'values' => $yearValues,
|
||||
'todayIndex' => $now->month - 1,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->get($this->apiUrl('/penugasan/' . $id));
|
||||
|
||||
if (!$response->successful()) {
|
||||
return redirect()->back()->with('error', 'Kegiatan tidak ditemukan.');
|
||||
}
|
||||
$data = $response->json()['data'];
|
||||
$activity = (object) [
|
||||
'id' => $data['id'],
|
||||
'nama_kegiatan' => $data['kegiatan'],
|
||||
'tanggal' => $data['tanggal_mulai'],
|
||||
'tanggal_selesai' => $data['tanggal_selesai'] ?? null,
|
||||
'status' => $data['status'] == 'proses' ? 'on_progres' : $data['status'],
|
||||
'deskripsi' => $data['keterangan'] ?? '-',
|
||||
'lokasi' => $data['lokasi'] ?? '-',
|
||||
'status_note' => $data['status_note'] ?? null,
|
||||
'updated_at' => \Carbon\Carbon::parse($data['updated_at'] ?? now()),
|
||||
'user' => (object) ['name' => $data['pembuat']['name'] ?? 'System'],
|
||||
'petugas' => $data['petugas'] ?? [],
|
||||
'nomor_surat' => $data['nomor_surat'] ?? '-',
|
||||
'progress' => $data['progress'] ?? []
|
||||
];
|
||||
|
||||
return view('superadmin.kegiatan.show', compact('activity'));
|
||||
}
|
||||
|
||||
public function approve(Request $request, $id)
|
||||
{
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->post($this->apiUrl("/penugasan/{$id}/update-status"), [
|
||||
'status' => 'telah_ditinjau'
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
return redirect()->back()->with('success', 'Kegiatan berhasil disetujui (Telah Ditinjau).');
|
||||
}
|
||||
|
||||
return redirect()->back()->with('error', 'Terjadi kesalahan saat menyetujui kegiatan.');
|
||||
}
|
||||
|
||||
public function revision(Request $request, $id)
|
||||
{
|
||||
$request->validate([
|
||||
'status_note' => 'required|string|min:5|max:255'
|
||||
]);
|
||||
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->post($this->apiUrl("/penugasan/{$id}/update-status"), [
|
||||
'status' => 'proses',
|
||||
'status_note' => 'REVISI dari ' . session('user_name') . ': ' . $request->status_note
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
return redirect()->back()->with('success', 'Kegiatan dikembalikan ke pegawai untuk direvisi.');
|
||||
}
|
||||
|
||||
return redirect()->back()->with('error', 'Terjadi kesalahan saat meminta revisi.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, $id)
|
||||
{
|
||||
$request->validate([
|
||||
'alasan_hapus' => 'required|string|min:10|max:500',
|
||||
]);
|
||||
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->post($this->apiUrl("/penugasan/{$id}/delete"), [
|
||||
'alasan' => $request->alasan_hapus
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
return redirect()->back()->with('success', 'Kegiatan berhasil dibatalkan dan dicatat dalam log.');
|
||||
}
|
||||
|
||||
return redirect()->back()->with('error', 'Gagal membatalkan kegiatan.');
|
||||
}
|
||||
|
||||
public function arsip()
|
||||
{
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->get($this->apiUrl('/penugasan'), ['is_archived' => 'true']);
|
||||
|
||||
$items = collect([]);
|
||||
if ($response->successful()) {
|
||||
$items = collect($response->json()['data'])->map(function($item) {
|
||||
return (object) [
|
||||
'id' => $item['id'],
|
||||
'nama_kegiatan' => $item['kegiatan'],
|
||||
'tanggal' => $item['tanggal_mulai'],
|
||||
'tanggal_selesai' => $item['tanggal_selesai'] ?? null,
|
||||
'created_at' => \Carbon\Carbon::parse($item['created_at']),
|
||||
'status' => $item['status'] == 'proses' ? 'on_progres' : $item['status'],
|
||||
'user' => (object) ['name' => $item['pembuat']['name'] ?? 'System'],
|
||||
'petugas' => $item['petugas'] ?? [],
|
||||
'pembuat' => $item['pembuat'] ?? []
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
// Manual Pagination
|
||||
$perPage = 10;
|
||||
$currentPage = \Illuminate\Pagination\Paginator::resolveCurrentPage();
|
||||
$currentItems = $items->slice(($currentPage - 1) * $perPage, $perPage)->all();
|
||||
$activities = new \Illuminate\Pagination\LengthAwarePaginator($currentItems, count($items), $perPage, $currentPage, [
|
||||
'path' => \Illuminate\Pagination\Paginator::resolveCurrentPath()
|
||||
]);
|
||||
|
||||
return view('superadmin.arsip.index', compact('activities'));
|
||||
}
|
||||
|
||||
public function archive($id)
|
||||
{
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->post($this->apiUrl("/penugasan/{$id}/archive"));
|
||||
|
||||
if ($response->successful()) {
|
||||
return redirect()->back()->with('success', 'Status arsip laporan berhasil diperbarui.');
|
||||
}
|
||||
|
||||
return redirect()->back()->with('error', 'Gagal memperbarui status arsip laporan.');
|
||||
}
|
||||
|
||||
public function userIndex()
|
||||
{
|
||||
// Panggil API Penugasan untuk menghitung activities dinamis
|
||||
$penugasanResponse = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->get($this->apiUrl('/penugasan'));
|
||||
$items = collect([]);
|
||||
if ($penugasanResponse->successful()) {
|
||||
$items = collect($penugasanResponse->json()['data']);
|
||||
}
|
||||
|
||||
// Panggil API Users
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->get($this->apiUrl('/users'));
|
||||
|
||||
$users = collect([]);
|
||||
if ($response->successful()) {
|
||||
$users = collect($response->json()['data'])->map(function($u) use ($items) {
|
||||
$activitiesCount = $items->filter(function($item) use ($u) {
|
||||
// Cek apakah user ada di array petugas
|
||||
if (isset($item['petugas']) && is_array($item['petugas'])) {
|
||||
foreach ($item['petugas'] as $p) {
|
||||
if ($p['id'] == $u['id']) return true;
|
||||
}
|
||||
}
|
||||
// Fallback cek pembuat
|
||||
return ($item['pembuat']['id'] ?? null) == $u['id'];
|
||||
})->count();
|
||||
|
||||
return (object) [
|
||||
'id' => $u['id'],
|
||||
'name' => $u['name'],
|
||||
'username' => $u['username'],
|
||||
'role' => $u['role'],
|
||||
'created_at' => isset($u['created_at']) ? \Carbon\Carbon::parse($u['created_at']) : now(),
|
||||
'activities_count' => $activitiesCount
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
// Manual Pagination
|
||||
$perPage = 15;
|
||||
$currentPage = \Illuminate\Pagination\Paginator::resolveCurrentPage();
|
||||
$currentItems = $users->slice(($currentPage - 1) * $perPage, $perPage)->all();
|
||||
$paginatedUsers = new \Illuminate\Pagination\LengthAwarePaginator($currentItems, $users->count(), $perPage, $currentPage, [
|
||||
'path' => \Illuminate\Pagination\Paginator::resolveCurrentPath()
|
||||
]);
|
||||
|
||||
$stats = [
|
||||
'total' => $users->count(),
|
||||
'active' => $users->where('activities_count', '>', 0)->count(),
|
||||
'activities' => $users->sum('activities_count'),
|
||||
'max_activities' => max($users->max('activities_count') ?? 1, 1),
|
||||
];
|
||||
|
||||
return view('superadmin.user.index', ['users' => $paginatedUsers, 'stats' => $stats]);
|
||||
}
|
||||
|
||||
public function createUser()
|
||||
{
|
||||
return view('superadmin.user.create-user');
|
||||
}
|
||||
|
||||
public function storeUser(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string',
|
||||
'username' => 'required|string',
|
||||
'password' => 'required|string|min:6',
|
||||
'role' => 'required|in:user,admin,superadmin',
|
||||
]);
|
||||
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->acceptJson()
|
||||
->post($this->apiUrl('/register'), [
|
||||
'name' => $request->name,
|
||||
'username' => $request->username,
|
||||
'password' => $request->password,
|
||||
'role' => $request->role,
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
return redirect()->route('admin.superadmin.user.index')->with('success', 'Pengguna berhasil ditambahkan');
|
||||
}
|
||||
|
||||
if ($response->status() === 422) {
|
||||
return back()->withErrors($response->json('errors'))->withInput();
|
||||
}
|
||||
|
||||
return back()->with('error', $response->json('message') ?? 'Gagal menambah pengguna')->withInput();
|
||||
}
|
||||
|
||||
public function editUser($id)
|
||||
{
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->get($this->apiUrl("/users/{$id}"));
|
||||
|
||||
if ($response->successful()) {
|
||||
$userData = $response->json()['data'];
|
||||
$user = (object) [
|
||||
'id' => $userData['id'],
|
||||
'name' => $userData['name'],
|
||||
'username' => $userData['username'],
|
||||
'role' => $userData['role'],
|
||||
'email' => $userData['email'] ?? '',
|
||||
'created_at' => isset($userData['created_at'])
|
||||
? \Carbon\Carbon::parse($userData['created_at'])
|
||||
: now(),
|
||||
];
|
||||
return view('superadmin.user.edit-user', compact('user'));
|
||||
}
|
||||
return redirect()->back()->with('error', 'User tidak ditemukan atau gagal mengambil data.');
|
||||
}
|
||||
|
||||
public function updateUser(Request $request, $id)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required',
|
||||
'username' => 'required',
|
||||
'role' => 'required',
|
||||
'password' => 'nullable|min:6|confirmed',
|
||||
]);
|
||||
|
||||
$payload = $request->only('name', 'username', 'role');
|
||||
if ($request->filled('password')) {
|
||||
$payload['password'] = $request->password;
|
||||
}
|
||||
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->acceptJson()
|
||||
->put($this->apiUrl("/users/{$id}"), $payload);
|
||||
|
||||
if ($response->successful()) {
|
||||
return redirect()->route('admin.superadmin.user.index')->with('success', 'User berhasil diupdate');
|
||||
}
|
||||
|
||||
if ($response->status() === 422) {
|
||||
return back()->withErrors($response->json('errors'))->withInput();
|
||||
}
|
||||
|
||||
return redirect()->back()->with('error', 'Gagal update user.')->withInput();
|
||||
}
|
||||
|
||||
// 🔥 DELETE USER
|
||||
public function destroyUser($id)
|
||||
{
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->delete($this->apiUrl('/users/' . $id));
|
||||
|
||||
if ($response->successful()) {
|
||||
return redirect()->route('admin.superadmin.user.index')->with('success', 'Pengguna berhasil dihapus');
|
||||
}
|
||||
|
||||
return back()->with('error', 'Gagal menghapus pengguna');
|
||||
}
|
||||
|
||||
// 🔥 CREATE KEGIATAN
|
||||
public function createKegiatan()
|
||||
{
|
||||
// Ambil data users untuk dropdown pilihan pegawai (opsional)
|
||||
$usersResponse = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->get($this->apiUrl('/users'));
|
||||
|
||||
$users = [];
|
||||
if ($usersResponse->successful()) {
|
||||
$allUsers = $usersResponse->json()['data'] ?? [];
|
||||
// Hanya tampilkan user dengan role "user" (Pegawai/Petugas)
|
||||
$users = array_filter($allUsers, function($u) {
|
||||
return isset($u['role']) && strtolower($u['role']) === 'user';
|
||||
});
|
||||
}
|
||||
|
||||
return view('superadmin.kegiatan.create', compact('users'));
|
||||
}
|
||||
|
||||
// 🔥 STORE KEGIATAN
|
||||
public function storeKegiatan(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'nomor_surat' => 'required|string',
|
||||
'kegiatan' => 'required|string',
|
||||
'tanggal_mulai' => 'required|date',
|
||||
'tanggal_selesai' => 'nullable|date',
|
||||
'pegawai_ids' => 'required|array',
|
||||
]);
|
||||
|
||||
$payload = [
|
||||
'nomor_surat' => $request->nomor_surat,
|
||||
'kegiatan' => $request->kegiatan,
|
||||
'tanggal_mulai' => $request->tanggal_mulai,
|
||||
'tanggal_selesai' => $request->tanggal_selesai,
|
||||
'pegawai_ids' => $request->pegawai_ids,
|
||||
];
|
||||
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->acceptJson()
|
||||
->post($this->apiUrl('/penugasan'), $payload);
|
||||
|
||||
if ($response->successful()) {
|
||||
return redirect()->route('admin.superadmin.dashboard')->with('success', 'Tugas baru berhasil dibuat');
|
||||
}
|
||||
|
||||
if ($response->status() === 422) {
|
||||
return back()->withErrors($response->json('errors'))->withInput();
|
||||
}
|
||||
|
||||
return back()->with('error', 'Gagal membuat tugas baru: ' . $response->json('message', 'Error API'))->withInput();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function export(Request $request)
|
||||
{
|
||||
$columns = $request->input('columns', []);
|
||||
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->get($this->apiUrl('/export-penugasan'), [
|
||||
'columns' => $columns
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$fileName = 'Laporan_Penugasan.xlsx';
|
||||
$contentDisposition = $response->header('Content-Disposition');
|
||||
|
||||
if ($contentDisposition && preg_match('/filename="?([^"]+)"?/', $contentDisposition, $matches)) {
|
||||
$fileName = $matches[1];
|
||||
}
|
||||
|
||||
return response($response->body())
|
||||
->header('Content-Type', $response->header('Content-Type') ?? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||
->header('Content-Disposition', 'attachment; filename="' . $fileName . '"');
|
||||
}
|
||||
|
||||
return back()->with('error', 'Gagal mengunduh laporan: Endpoint backend mungkin bermasalah atau tidak tersedia.');
|
||||
}
|
||||
|
||||
public function chartData(Request $request)
|
||||
{
|
||||
$mode = $request->get('mode', 'week');
|
||||
$response = \Illuminate\Support\Facades\Http::withToken(session('api_token'))
|
||||
->get($this->apiUrl('/penugasan'));
|
||||
|
||||
$items = collect([]);
|
||||
if ($response->successful()) {
|
||||
$items = collect($response->json()['data'])->filter(function($item) {
|
||||
return $item['status'] !== 'dibatalkan';
|
||||
})->map(function($item) {
|
||||
return (object) ['created_at' => \Carbon\Carbon::parse($item['created_at'])];
|
||||
});
|
||||
}
|
||||
$data = $this->buildChartData($items);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $data[$mode] ?? $data['week'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class NotificationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mendapatkan URL API.
|
||||
*/
|
||||
protected function apiUrl($endpoint)
|
||||
{
|
||||
return rtrim(env('BACKEND_API_URL'), '/') . '/' . ltrim($endpoint, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Mengambil daftar notifikasi dari Backend.
|
||||
*/
|
||||
public function getNotifications()
|
||||
{
|
||||
if (!session()->has('api_token')) {
|
||||
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$response = Http::withToken(session('api_token'))
|
||||
->get($this->apiUrl('/notifications'));
|
||||
|
||||
if ($response->successful()) {
|
||||
return response()->json($response->json());
|
||||
}
|
||||
|
||||
return response()->json(['success' => false, 'message' => 'Gagal memuat notifikasi'], 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Menandai semua notifikasi telah dibaca di Backend.
|
||||
*/
|
||||
public function markAsRead()
|
||||
{
|
||||
if (!session()->has('api_token')) {
|
||||
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$response = Http::withToken(session('api_token'))
|
||||
->post($this->apiUrl('/notifications/read'));
|
||||
|
||||
if ($response->successful()) {
|
||||
return response()->json($response->json());
|
||||
}
|
||||
|
||||
return response()->json(['success' => false, 'message' => 'Gagal menandai telah dibaca'], 500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class UserLoginController extends Controller
|
||||
{
|
||||
/**
|
||||
* Helper URL API Backend
|
||||
*/
|
||||
private function apiUrl($endpoint = '')
|
||||
{
|
||||
return rtrim(env('BACKEND_API_URL'), '/') . '/' . ltrim($endpoint, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* PROSES LOGIN
|
||||
* Login tidak lagi membaca database KP-ASOY.
|
||||
* Login dikirim ke API Laravel_KP.
|
||||
*/
|
||||
public function login(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'username' => 'required',
|
||||
'password' => 'required',
|
||||
]);
|
||||
|
||||
try {
|
||||
$response = Http::post($this->apiUrl('/login'), [
|
||||
'username' => $request->username,
|
||||
'password' => $request->password,
|
||||
]);
|
||||
|
||||
if (!$response->successful()) {
|
||||
Log::error('Login API gagal', [
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body(),
|
||||
]);
|
||||
|
||||
return back()->with('error', 'Username atau password salah.');
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
if (!isset($data['token']) || !isset($data['user'])) {
|
||||
Log::error('Format response login API tidak sesuai', [
|
||||
'response' => $data,
|
||||
]);
|
||||
|
||||
return back()->with('error', 'Login gagal. Response backend tidak sesuai.');
|
||||
}
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
session([
|
||||
'api_token' => $data['token'],
|
||||
'user' => $data['user'],
|
||||
'user_id' => $data['user']['id'] ?? null,
|
||||
'user_name' => $data['user']['name'] ?? null,
|
||||
'username' => $data['user']['username'] ?? null,
|
||||
'role' => $data['user']['role'] ?? 'user',
|
||||
]);
|
||||
|
||||
return $this->redirectUser($data['user']);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Tidak dapat terhubung ke backend API', [
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return back()->with('error', 'Tidak dapat terhubung ke backend. Pastikan Laravel_KP berjalan di port 8001.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* REDIRECT BERDASARKAN ROLE DARI BACKEND
|
||||
*/
|
||||
private function redirectUser($user)
|
||||
{
|
||||
$role = $user['role'] ?? 'user';
|
||||
|
||||
if ($role === 'admin') {
|
||||
return redirect()->route('admin.dashboard');
|
||||
}
|
||||
|
||||
if ($role === 'superadmin') {
|
||||
return redirect()->route('admin.superadmin.dashboard');
|
||||
}
|
||||
|
||||
return redirect()->route('user.dashboard');
|
||||
}
|
||||
|
||||
/**
|
||||
* PROSES LOGOUT
|
||||
* Logout dari frontend dan backend API.
|
||||
*/
|
||||
public function logout(Request $request)
|
||||
{
|
||||
try {
|
||||
if (session('api_token')) {
|
||||
Http::withToken(session('api_token'))
|
||||
->post($this->apiUrl('/logout'));
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Logout API gagal atau endpoint logout belum tersedia', [
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect()->route('login');
|
||||
}
|
||||
|
||||
/**
|
||||
* USER SETTINGS
|
||||
* Update email dan password dikirim ke backend.
|
||||
*
|
||||
* Catatan:
|
||||
* Endpoint backend yang dibutuhkan:
|
||||
* PUT /api/profile/update
|
||||
*/
|
||||
public function updateSettings(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'username' => 'required|string',
|
||||
'current_password' => 'nullable|string',
|
||||
'password' => 'nullable|min:6|confirmed',
|
||||
], [
|
||||
'password.confirmed' => 'Konfirmasi kata sandi tidak cocok.'
|
||||
]);
|
||||
|
||||
try {
|
||||
$payload = [
|
||||
'username' => $request->username,
|
||||
];
|
||||
|
||||
if ($request->filled('password')) {
|
||||
if (session('role') === 'user' || session('user')['role'] === 'user') {
|
||||
return back()->with('error', 'Pegawai tidak diizinkan mengubah kata sandi.');
|
||||
}
|
||||
$payload['password'] = $request->password;
|
||||
$payload['current_password'] = $request->current_password;
|
||||
}
|
||||
|
||||
$response = Http::withToken(session('api_token'))
|
||||
->put($this->apiUrl('/profile/update'), $payload);
|
||||
|
||||
if ($response->status() === 400) {
|
||||
return back()->with('error', $response->json('message', 'Terjadi kesalahan pada input kata sandi.'));
|
||||
}
|
||||
|
||||
if (!$response->successful()) {
|
||||
Log::error('Update akun gagal', [
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body(),
|
||||
]);
|
||||
|
||||
return back()->with('error', 'Gagal memperbarui akun: ' . $response->json('message', ''));
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
if (isset($data['user'])) {
|
||||
session(['user' => $data['user']]);
|
||||
session(['user_name' => $data['user']['name']]);
|
||||
session(['username' => $data['user']['username']]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Berhasil update akun!');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Update akun gagal terhubung backend', [
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return back()->with('error', 'Tidak dapat terhubung ke backend.');
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| FITUR MANAJEMEN USER / PEGAWAI
|
||||
|--------------------------------------------------------------------------
|
||||
| Admin membuat akun melalui backend API Laravel_KP.
|
||||
*/
|
||||
|
||||
/**
|
||||
* ADMIN - LIST SEMUA USER
|
||||
*
|
||||
* Catatan:
|
||||
* Endpoint backend yang dibutuhkan:
|
||||
* GET /api/users
|
||||
*/
|
||||
public function userList()
|
||||
{
|
||||
try {
|
||||
$response = Http::withToken(session('api_token'))
|
||||
->acceptJson()
|
||||
->get($this->apiUrl('/users'));
|
||||
|
||||
if (!$response->successful()) {
|
||||
Log::error('Gagal mengambil data user dari backend', [
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body(),
|
||||
]);
|
||||
|
||||
$users = collect([]);
|
||||
return view('admin.users', compact('users'))
|
||||
->with('error', 'Gagal mengambil data user dari backend.');
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
$rawUsers = $data['data'] ?? $data['users'] ?? $data ?? [];
|
||||
// Bungkus sebagai Collection agar metode seperti ->where() dapat digunakan di view
|
||||
$users = collect($rawUsers)->map(fn($u) => (object)$u);
|
||||
|
||||
return view('admin.users', compact('users'));
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Gagal terhubung backend saat mengambil user', [
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
$users = [];
|
||||
return view('admin.users', compact('users'))
|
||||
->with('error', 'Tidak dapat terhubung ke backend.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ADMIN - FORM TAMBAH USER
|
||||
*/
|
||||
public function createUser()
|
||||
{
|
||||
return view('admin.create-user');
|
||||
}
|
||||
|
||||
/**
|
||||
* ADMIN - SIMPAN USER BARU
|
||||
*
|
||||
* Catatan:
|
||||
* Endpoint backend yang dibutuhkan:
|
||||
* POST /api/register
|
||||
* atau POST /api/users
|
||||
*/
|
||||
public function storeUser(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required',
|
||||
'username' => 'required',
|
||||
'password' => 'required|min:6',
|
||||
'jabatan' => 'nullable',
|
||||
'role' => 'nullable',
|
||||
]);
|
||||
|
||||
try {
|
||||
$response = Http::withToken(session('api_token'))
|
||||
->acceptJson()
|
||||
->post($this->apiUrl('/register'), [
|
||||
'name' => $request->name,
|
||||
'username' => $request->username,
|
||||
'password' => $request->password,
|
||||
'jabatan' => $request->jabatan,
|
||||
'role' => $request->role ?? 'user',
|
||||
]);
|
||||
|
||||
if ($response->status() === 422) {
|
||||
$errors = collect($response->json('errors', []))->flatten()->toArray();
|
||||
return back()->withInput()->withErrors($errors);
|
||||
}
|
||||
|
||||
if (!$response->successful()) {
|
||||
Log::error('Gagal membuat user lewat backend', [
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body(),
|
||||
]);
|
||||
return back()->withInput()
|
||||
->with('error', 'User gagal ditambahkan. ' . ($response->json('message') ?? ''));
|
||||
}
|
||||
|
||||
return redirect()->route('admin.users')
|
||||
->with('success', 'User berhasil ditambahkan!');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Gagal terhubung backend saat membuat user', ['message' => $e->getMessage()]);
|
||||
return back()->withInput()->with('error', 'Tidak dapat terhubung ke backend.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ADMIN - FORM EDIT USER
|
||||
*/
|
||||
public function editUser($id)
|
||||
{
|
||||
try {
|
||||
$response = Http::withToken(session('api_token'))
|
||||
->acceptJson()
|
||||
->get($this->apiUrl('/users/' . $id));
|
||||
|
||||
if (!$response->successful()) {
|
||||
return redirect()->route('admin.users')
|
||||
->with('error', 'Pengguna tidak ditemukan.');
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
$user = (object)($data['data'] ?? $data['user'] ?? $data);
|
||||
|
||||
if (($user->role ?? '') === 'superadmin' && session('user')['role'] !== 'superadmin') {
|
||||
return redirect()->route('admin.users')
|
||||
->with('error', 'Akses ditolak. Anda tidak dapat mengedit Super Admin.');
|
||||
}
|
||||
|
||||
return view('admin.edit-user', compact('user'));
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Gagal mengambil data user untuk edit', ['message' => $e->getMessage()]);
|
||||
return redirect()->route('admin.users')
|
||||
->with('error', 'Tidak dapat terhubung ke backend.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ADMIN - UPDATE USER
|
||||
*/
|
||||
public function updateUser(Request $request, $id)
|
||||
{
|
||||
try {
|
||||
$checkResponse = Http::withToken(session('api_token'))->acceptJson()->get($this->apiUrl('/users/' . $id));
|
||||
if ($checkResponse->successful()) {
|
||||
$checkData = $checkResponse->json();
|
||||
$checkUser = (object)($checkData['data'] ?? $checkData['user'] ?? $checkData);
|
||||
if (($checkUser->role ?? '') === 'superadmin' && session('user')['role'] !== 'superadmin') {
|
||||
return redirect()->route('admin.users')->with('error', 'Akses ditolak. Anda tidak dapat mengubah data Super Admin.');
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Continue if check fails, API might still reject it
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'name' => 'required',
|
||||
'username' => 'required',
|
||||
'jabatan' => 'nullable',
|
||||
'role' => 'nullable',
|
||||
'password' => 'nullable|min:6',
|
||||
]);
|
||||
|
||||
try {
|
||||
$payload = [
|
||||
'name' => $request->name,
|
||||
'username' => $request->username,
|
||||
'jabatan' => $request->jabatan,
|
||||
'role' => $request->role,
|
||||
];
|
||||
if ($request->filled('password')) {
|
||||
$payload['password'] = $request->password;
|
||||
}
|
||||
|
||||
$response = Http::withToken(session('api_token'))
|
||||
->acceptJson()
|
||||
->put($this->apiUrl('/users/' . $id), $payload);
|
||||
|
||||
if ($response->status() === 422) {
|
||||
$errors = collect($response->json('errors', []))->flatten()->toArray();
|
||||
return back()->withInput()->withErrors($errors);
|
||||
}
|
||||
|
||||
if (!$response->successful()) {
|
||||
return back()->withInput()
|
||||
->with('error', 'Gagal memperbarui pengguna: ' . ($response->json('message') ?? ''));
|
||||
}
|
||||
|
||||
return redirect()->route('admin.users')
|
||||
->with('success', 'Data pengguna berhasil diperbarui!');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Gagal update user', ['message' => $e->getMessage()]);
|
||||
return back()->withInput()->with('error', 'Tidak dapat terhubung ke backend.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ADMIN - HAPUS USER
|
||||
*
|
||||
* Catatan:
|
||||
* Untuk revisi KP, admin sebaiknya hanya membuat akun.
|
||||
* Hapus user lebih aman hanya untuk superadmin.
|
||||
*
|
||||
* Endpoint backend yang dibutuhkan:
|
||||
* DELETE /api/users/{id}
|
||||
*/
|
||||
public function destroyUser($id)
|
||||
{
|
||||
try {
|
||||
if ((int) $id === (int) session('user_id')) {
|
||||
return back()->with('error', 'Anda tidak bisa menghapus akun sendiri!');
|
||||
}
|
||||
|
||||
$checkResponse = Http::withToken(session('api_token'))->acceptJson()->get($this->apiUrl('/users/' . $id));
|
||||
if ($checkResponse->successful()) {
|
||||
$checkData = $checkResponse->json();
|
||||
$checkUser = (object)($checkData['data'] ?? $checkData['user'] ?? $checkData);
|
||||
if (($checkUser->role ?? '') === 'superadmin' && session('user')['role'] !== 'superadmin') {
|
||||
return back()->with('error', 'Akses ditolak. Anda tidak dapat menghapus Super Admin.');
|
||||
}
|
||||
}
|
||||
|
||||
$response = Http::withToken(session('api_token'))
|
||||
->acceptJson()
|
||||
->delete($this->apiUrl('/users/' . $id));
|
||||
|
||||
if (!$response->successful()) {
|
||||
Log::error('Gagal menghapus user lewat backend', [
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body(),
|
||||
]);
|
||||
|
||||
return back()->with('error', 'User gagal dihapus.');
|
||||
}
|
||||
|
||||
return back()->with('success', 'User berhasil dihapus!');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Gagal terhubung backend saat hapus user', [
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return back()->with('error', 'Tidak dapat terhubung ke backend.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class RoleMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, ...$roles): Response
|
||||
{
|
||||
// Ambil user dari session
|
||||
$user = session('user');
|
||||
|
||||
// Jika belum login
|
||||
if (!$user) {
|
||||
return redirect()->route('login')
|
||||
->with('error', 'Silakan login terlebih dahulu');
|
||||
}
|
||||
|
||||
// Jika role tidak ada
|
||||
if (!isset($user['role'])) {
|
||||
abort(403, 'Role tidak ditemukan');
|
||||
}
|
||||
|
||||
// Jika role tidak sesuai
|
||||
if (!in_array($user['role'], $roles)) {
|
||||
$role = $user['role'];
|
||||
if ($role === 'admin') {
|
||||
return redirect()->route('admin.dashboard')->with('error', 'Akses ditolak: Anda tidak memiliki izin untuk halaman tersebut.');
|
||||
} elseif ($role === 'superadmin') {
|
||||
return redirect()->route('admin.superadmin.dashboard')->with('error', 'Akses ditolak: Anda tidak memiliki izin untuk halaman tersebut.');
|
||||
} else {
|
||||
return redirect()->route('user.dashboard')->with('error', 'Akses ditolak: Anda tidak memiliki izin untuk halaman tersebut.');
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes; // ← TAMBAHAN
|
||||
|
||||
class Activity extends Model
|
||||
{
|
||||
use SoftDeletes; // ← TAMBAHAN
|
||||
|
||||
protected $fillable = [
|
||||
'nama_kegiatan',
|
||||
'keterangan',
|
||||
'status',
|
||||
'user_id'
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* Kolom yang boleh diisi (Mass Assignment)
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'username',
|
||||
'email',
|
||||
'password',
|
||||
'role',
|
||||
];
|
||||
|
||||
/**
|
||||
* Kolom yang disembunyikan
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Casting format data
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed', // Laravel akan otomatis nge-hash saat save()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relasi ke Activities
|
||||
*/
|
||||
public function activities()
|
||||
{
|
||||
return $this->hasMany(Activity::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
View::composer('layouts.navbar', function ($view) {
|
||||
if (session()->has('api_token')) {
|
||||
$notifications = [
|
||||
['text' => 'Kegiatan sudah ditinjau super admin', 'is_read' => false],
|
||||
['text' => 'Aktivitas berhasil disimpan', 'is_read' => true],
|
||||
['text' => 'Perubahan data tersimpan', 'is_read' => true],
|
||||
];
|
||||
$unreadCount = collect($notifications)->where('is_read', false)->count();
|
||||
$view->with(compact('notifications', 'unreadCount'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,24 @@
|
||||
<?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) {
|
||||
$middleware->alias([
|
||||
'role' => \App\Http\Middleware\RoleMiddleware::class,
|
||||
]);
|
||||
})
|
||||
|
||||
->withExceptions(function (Exceptions $exceptions) {
|
||||
//
|
||||
})
|
||||
|
||||
->create();
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Providers\AppServiceProvider;
|
||||
|
||||
return [
|
||||
AppServiceProvider::class,
|
||||
];
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"$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",
|
||||
"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 --ignore-scripts",
|
||||
"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
|
||||
}
|
||||
Generated
+8242
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' => 'Asia/Jakarta',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Locale Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The application locale determines the default locale that will be used
|
||||
| by Laravel's translation / localization methods. This option can be
|
||||
| set to any locale for which you plan to have translation strings.
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
|
||||
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This key is utilized by Laravel's encryption services and should be set
|
||||
| to a random, 32 character string to ensure that all encrypted values
|
||||
| are secure. You should do this prior to deploying the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||
),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maintenance Mode Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options determine the driver used to determine and
|
||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||
| allow maintenance mode to be controlled across multiple machines.
|
||||
|
|
||||
| Supported drivers: "file", "cache"
|
||||
|
|
||||
*/
|
||||
|
||||
'maintenance' => [
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -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,49 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->bigInteger('expiration')->index();
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->bigInteger('expiration')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedSmallInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('activities', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
|
||||
$table->string('nama_kegiatan');
|
||||
$table->string('status')->default('on progress');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('activities');
|
||||
}
|
||||
};
|
||||
+28
@@ -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('activities', function (Blueprint $table) {
|
||||
$table->date('tanggal')->nullable()->after('nama_kegiatan');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('activities', function (Blueprint $table) {
|
||||
//
|
||||
});
|
||||
}
|
||||
};
|
||||
+28
@@ -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('activities', function (Blueprint $table) {
|
||||
$table->softDeletes(); // ← tambah kolom deleted_at
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('activities', function (Blueprint $table) {
|
||||
$table->dropSoftDeletes(); // ← hapus kolom deleted_at jika rollback
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
use WithoutModelEvents;
|
||||
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
}
|
||||
}
|
||||
+1614
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://www.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.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>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 960 KiB |
@@ -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 @@
|
||||
//
|
||||
@@ -0,0 +1,60 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('content')
|
||||
{{-- Flex container untuk menarik konten ke tengah horizontal --}}
|
||||
<div class="flex justify-center items-center w-full pb-20">
|
||||
|
||||
<div class="w-full max-w-lg"> {{-- max-w-lg membatasi lebar form supaya simetris --}}
|
||||
|
||||
<div class="mb-8 text-center">
|
||||
<h1 class="text-3xl font-extrabold text-[#0D215C] tracking-tight">Tambah Petugas Baru</h1>
|
||||
<p class="text-slate-500 mt-2 font-medium">Buat akun untuk pengawas lapangan</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-[2.5rem] shadow-2xl shadow-slate-200/60 border border-slate-100 overflow-hidden">
|
||||
<div class="p-8 md:p-12">
|
||||
|
||||
<form action="{{ route('admin.users.store') }}" method="POST" class="space-y-6">
|
||||
@csrf
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-[11px] font-black text-slate-400 uppercase tracking-[2px] ml-1">Nama Lengkap</label>
|
||||
<input type="text" name="name"
|
||||
class="w-full px-6 py-4 bg-[#F2F5FB] border-none rounded-2xl focus:ring-4 focus:ring-[#0D215C]/10 transition-all outline-none text-slate-700 placeholder:text-slate-300"
|
||||
placeholder="Contoh: Budi Santoso" required>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-[11px] font-black text-slate-400 uppercase tracking-[2px] ml-1">Username</label>
|
||||
<input type="text" name="username"
|
||||
class="w-full px-6 py-4 bg-[#F2F5FB] border-none rounded-2xl focus:ring-4 focus:ring-[#0D215C]/10 transition-all outline-none text-slate-700 placeholder:text-slate-300"
|
||||
placeholder="budi_petugas" required>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-[11px] font-black text-slate-400 uppercase tracking-[2px] ml-1">Password</label>
|
||||
<input type="password" name="password"
|
||||
class="w-full px-6 py-4 bg-[#F2F5FB] border-none rounded-2xl focus:ring-4 focus:ring-[#0D215C]/10 transition-all outline-none text-slate-700 placeholder:text-slate-300"
|
||||
placeholder="Masukkan password" required>
|
||||
</div>
|
||||
|
||||
<div class="pt-6">
|
||||
<button type="submit"
|
||||
class="w-full bg-[#0D215C] text-white font-bold py-5 rounded-2xl shadow-xl shadow-blue-900/20 hover:bg-[#081a45] transition-all transform active:scale-95">
|
||||
Simpan Akun Petugas
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 text-center">
|
||||
<a href="{{ route('admin.users') }}" class="text-sm font-bold text-slate-400 hover:text-[#0D215C] transition-colors flex items-center justify-center">
|
||||
<i data-lucide="arrow-left" class="w-4 h-4 mr-2"></i> KEMBALI KE DAFTAR
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,913 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('content')
|
||||
|
||||
<style>
|
||||
/* ══════════════════════════════════════════════
|
||||
DISNAKER KALBAR – ADMIN DASHBOARD
|
||||
Professional Government Dashboard UI
|
||||
══════════════════════════════════════════════ */
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap');
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
.d-wrap {
|
||||
padding: 2rem 2.5rem 3rem;
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
}
|
||||
|
||||
/* ── Page Header ─────────────────────────────── */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
.page-header-left h2 {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #0D215C;
|
||||
letter-spacing: -0.3px;
|
||||
line-height: 1;
|
||||
}
|
||||
.page-header-left .subtitle {
|
||||
font-size: 12.5px;
|
||||
color: #94a3b8;
|
||||
font-weight: 500;
|
||||
margin-top: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.status-dot {
|
||||
width: 7px; height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #22c55e;
|
||||
display: inline-block;
|
||||
box-shadow: 0 0 0 3px rgba(34,197,94,0.2);
|
||||
animation: pulse-dot 2s infinite;
|
||||
}
|
||||
@keyframes pulse-dot {
|
||||
0%,100% { box-shadow: 0 0 0 3px rgba(34,197,94,0.2); }
|
||||
50% { box-shadow: 0 0 0 6px rgba(34,197,94,0.08); }
|
||||
}
|
||||
.page-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.hdr-chip {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 7px 18px;
|
||||
border-radius: 50px;
|
||||
letter-spacing: 0.4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.hdr-chip.navy { background: #0D215C; color: #fff; box-shadow: 0 4px 14px rgba(13,33,92,0.22); }
|
||||
.hdr-chip.light { background: #fff; color: #64748b; border: 1px solid #e2e8f0; }
|
||||
.hdr-chip svg { flex-shrink: 0; }
|
||||
|
||||
/* ── Stat Cards ──────────────────────────────── */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 18px;
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
.s-card {
|
||||
background: #fff;
|
||||
border-radius: 20px;
|
||||
padding: 1.5rem 1.6rem 1.4rem;
|
||||
border: 1px solid #edf1f7;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: default;
|
||||
transition: transform 0.22s cubic-bezier(.4,0,.2,1),
|
||||
box-shadow 0.22s cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
.s-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 16px 40px rgba(13,33,92,0.12);
|
||||
}
|
||||
.s-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0; left: 0; right: 0;
|
||||
height: 4px;
|
||||
border-radius: 0 0 20px 20px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.s-card.c-blue::after { background: linear-gradient(90deg,#3b82f6,#6366f1); }
|
||||
.s-card.c-green::after { background: linear-gradient(90deg,#22c55e,#10b981); }
|
||||
.s-card.c-yellow::after { background: linear-gradient(90deg,#f59e0b,#f97316); }
|
||||
.s-card.c-red::after { background: linear-gradient(90deg,#64748b,#475569); }
|
||||
.s-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -20px; right: -20px;
|
||||
width: 100px; height: 100px;
|
||||
border-radius: 50%;
|
||||
opacity: 0.04;
|
||||
}
|
||||
.s-card.c-blue::before { background: #3b82f6; }
|
||||
.s-card.c-green::before { background: #22c55e; }
|
||||
.s-card.c-yellow::before { background: #f59e0b; }
|
||||
.s-card.c-red::before { background: #64748b; }
|
||||
|
||||
.s-icon {
|
||||
width: 44px; height: 44px;
|
||||
border-radius: 12px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.s-icon.c-blue { background: #eff6ff; color: #3b82f6; }
|
||||
.s-icon.c-green { background: #f0fdf4; color: #16a34a; }
|
||||
.s-icon.c-yellow { background: #fffbeb; color: #d97706; }
|
||||
.s-icon.c-red { background: #f8fafc; color: #475569; }
|
||||
.s-icon svg { width: 20px; height: 20px; }
|
||||
|
||||
.s-num {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
letter-spacing: -2px;
|
||||
line-height: 1;
|
||||
}
|
||||
.s-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #94a3b8;
|
||||
margin-top: 5px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
.s-trend {
|
||||
font-size: 11.5px;
|
||||
font-weight: 500;
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.s-trend.up { color: #16a34a; }
|
||||
.s-trend.down { color: #94a3b8; }
|
||||
.s-trend.flat { color: #94a3b8; }
|
||||
.s-trend svg { width: 13px; height: 13px; }
|
||||
|
||||
/* ── Main Content Grid ───────────────────────── */
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 360px;
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* ── Generic Card ────────────────────────────── */
|
||||
.g-card {
|
||||
background: #fff;
|
||||
border-radius: 20px;
|
||||
padding: 1.5rem 1.6rem;
|
||||
border: 1px solid #edf1f7;
|
||||
box-shadow: 0 2px 12px rgba(13,33,92,0.04);
|
||||
}
|
||||
.g-card-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
.g-card-title h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #0D215C;
|
||||
letter-spacing: -0.1px;
|
||||
}
|
||||
.g-card-title a {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: gap 0.18s;
|
||||
}
|
||||
.g-card-title a:hover { gap: 8px; }
|
||||
|
||||
/* ── Divider ─────────────────────────────────── */
|
||||
.act-divider {
|
||||
height: 1px;
|
||||
background: #f1f5f9;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
/* ── Filter Tabs ─────────────────────────────── */
|
||||
.ftabs {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.ftab {
|
||||
padding: 5px 15px;
|
||||
border-radius: 50px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: 1.5px solid transparent;
|
||||
transition: all 0.15s;
|
||||
user-select: none;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
.ftab.on { background: #0D215C; color: #fff; border-color: #0D215C; }
|
||||
.ftab.off { background: #f8fafc; color: #94a3b8; border-color: #e2e8f0; }
|
||||
.ftab.off:hover { background: #eff6ff; color: #3b82f6; border-color: #bfdbfe; }
|
||||
|
||||
/* ── Activity Rows ───────────────────────────── */
|
||||
.act-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 13px;
|
||||
padding: 10px 10px;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, box-shadow 0.15s, transform 0.15s;
|
||||
}
|
||||
.act-item:hover {
|
||||
background: #f8fafc;
|
||||
box-shadow: 0 4px 16px rgba(13,33,92,0.06);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
.act-item:active { transform: scale(0.99); }
|
||||
|
||||
.act-ava {
|
||||
width: 38px; height: 38px;
|
||||
border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.av-1 { background: linear-gradient(135deg,#3b82f6,#6366f1); }
|
||||
.av-2 { background: linear-gradient(135deg,#14b8a6,#06b6d4); }
|
||||
.av-3 { background: linear-gradient(135deg,#a855f7,#ec4899); }
|
||||
.av-4 { background: linear-gradient(135deg,#f97316,#f59e0b); }
|
||||
.av-5 { background: linear-gradient(135deg,#22c55e,#10b981); }
|
||||
|
||||
.act-body { flex: 1; min-width: 0; }
|
||||
.act-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.act-meta {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.pill {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 4px 12px;
|
||||
border-radius: 50px;
|
||||
letter-spacing: 0.3px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.pill-ok { background: #dcfce7; color: #15803d; }
|
||||
.pill-run { background: #dbeafe; color: #1d4ed8; }
|
||||
.pill-del { background: #fee2e2; color: #b91c1c; }
|
||||
.pill-pnd { background: #fef9c3; color: #92400e; }
|
||||
|
||||
.empty-box {
|
||||
padding: 3.5rem 1rem;
|
||||
text-align: center;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
.empty-box .ei {
|
||||
width: 48px; height: 48px;
|
||||
margin: 0 auto 12px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: #f1f5f9;
|
||||
border-radius: 14px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.empty-box .ei svg { width: 22px; height: 22px; }
|
||||
.empty-box p { font-size: 13px; font-weight: 500; color: #94a3b8; }
|
||||
|
||||
/* ── Modal ────────────────────────────────────── */
|
||||
.modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(13,33,92,0.28);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 999;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
.modal-overlay.show { display: flex; }
|
||||
@keyframes fadeIn { from { opacity:0; } to { opacity:1; } }
|
||||
.modal-box {
|
||||
background: #fff;
|
||||
border-radius: 22px;
|
||||
padding: 2rem;
|
||||
width: 460px;
|
||||
max-width: 95vw;
|
||||
box-shadow: 0 30px 80px rgba(13,33,92,0.18);
|
||||
animation: slideUp 0.25s cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from { opacity:0; transform:translateY(20px); }
|
||||
to { opacity:1; transform:translateY(0); }
|
||||
}
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
.modal-header h4 {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0D215C;
|
||||
line-height: 1.4;
|
||||
max-width: 85%;
|
||||
}
|
||||
.modal-close {
|
||||
width: 30px; height: 30px;
|
||||
border-radius: 50%;
|
||||
background: #f1f5f9;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: #64748b;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.modal-close:hover { background: #fee2e2; color: #ef4444; }
|
||||
.modal-close svg { width: 14px; height: 14px; }
|
||||
.modal-field {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
font-size: 13px;
|
||||
}
|
||||
.modal-field:last-child { border-bottom: none; }
|
||||
.modal-field .mf-label {
|
||||
width: 120px;
|
||||
flex-shrink: 0;
|
||||
color: #94a3b8;
|
||||
font-weight: 500;
|
||||
font-size: 11.5px;
|
||||
letter-spacing: 0.3px;
|
||||
padding-top: 1px;
|
||||
}
|
||||
.modal-field .mf-value {
|
||||
color: #1e293b;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ── Sidebar ─────────────────────────────────── */
|
||||
.sidebar-col { display: flex; flex-direction: column; gap: 18px; }
|
||||
|
||||
/* Bar Chart */
|
||||
.chart-bars {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
height: 88px;
|
||||
margin: 4px 0 8px;
|
||||
}
|
||||
.bar-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.bar-body {
|
||||
width: 100%;
|
||||
border-radius: 6px 6px 0 0;
|
||||
background: #e0e7ff;
|
||||
margin-top: auto;
|
||||
transition: all 0.3s cubic-bezier(.4,0,.2,1);
|
||||
position: relative;
|
||||
}
|
||||
.bar-wrap:hover .bar-body {
|
||||
filter: brightness(0.9);
|
||||
transform: scaleY(1.04);
|
||||
transform-origin: bottom;
|
||||
}
|
||||
.bar-body::after {
|
||||
content: attr(data-val);
|
||||
position: absolute;
|
||||
top: -28px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #0D215C;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.bar-wrap:hover .bar-body::after { opacity: 1; }
|
||||
.bar-body.today { background: linear-gradient(180deg,#3b82f6 0%,#0D215C 100%); }
|
||||
.bar-body.active { background: #93c5fd; }
|
||||
.bar-lbl {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
}
|
||||
.bar-lbl.today-lbl { color: #3b82f6; font-weight: 600; }
|
||||
.chart-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
font-weight: 400;
|
||||
}
|
||||
.chart-footer span { color: #94a3b8; }
|
||||
.chart-footer strong { color: #0D215C; font-weight: 700; }
|
||||
|
||||
/* Progress */
|
||||
.prog-item { margin-bottom: 14px; }
|
||||
.prog-item:last-child { margin-bottom: 0; }
|
||||
.prog-head { display: flex; justify-content: space-between; margin-bottom: 7px; }
|
||||
.prog-head span { font-size: 12.5px; font-weight: 500; color: #334155; }
|
||||
.prog-head strong { font-size: 12.5px; font-weight: 700; color: #0D215C; }
|
||||
.prog-track { height: 8px; background: #f1f5f9; border-radius: 50px; overflow: hidden; }
|
||||
.prog-fill { height: 100%; border-radius: 50px; transition: width 1.2s cubic-bezier(.4,0,.2,1); }
|
||||
|
||||
/* Top Users */
|
||||
.usr-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 12px;
|
||||
transition: background 0.15s, box-shadow 0.15s;
|
||||
cursor: default;
|
||||
}
|
||||
.usr-row:hover {
|
||||
background: #f8fafc;
|
||||
box-shadow: 0 2px 10px rgba(13,33,92,0.05);
|
||||
}
|
||||
.usr-rank {
|
||||
width: 24px; height: 24px;
|
||||
border-radius: 7px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.rank-1 { background: #fef9c3; color: #b45309; }
|
||||
.rank-2 { background: #f1f5f9; color: #64748b; }
|
||||
.rank-3 { background: #fff7ed; color: #c2410c; }
|
||||
.rank-n { background: #f8fafc; color: #94a3b8; }
|
||||
.usr-ava {
|
||||
width: 32px; height: 32px;
|
||||
border-radius: 9px;
|
||||
background: linear-gradient(135deg,#0D215C,#2563eb);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.usr-name { flex: 1; font-size: 12.5px; font-weight: 500; color: #334155; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.usr-count {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
padding: 3px 11px;
|
||||
border-radius: 50px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Animations ──────────────────────────────── */
|
||||
@keyframes fadeUp {
|
||||
from { opacity:0; transform:translateY(16px); }
|
||||
to { opacity:1; transform:translateY(0); }
|
||||
}
|
||||
.s-card { animation: fadeUp 0.4s ease both; }
|
||||
.s-card:nth-child(1) { animation-delay:.06s; }
|
||||
.s-card:nth-child(2) { animation-delay:.12s; }
|
||||
.s-card:nth-child(3) { animation-delay:.18s; }
|
||||
.s-card:nth-child(4) { animation-delay:.24s; }
|
||||
.content-grid { animation: fadeUp 0.4s .28s ease both; }
|
||||
|
||||
/* ── Responsive ──────────────────────────────── */
|
||||
@media (max-width:1100px) { .content-grid { grid-template-columns:1fr; } }
|
||||
@media (max-width:768px) {
|
||||
.stats-grid { grid-template-columns:repeat(2,1fr); gap: 12px; }
|
||||
.d-wrap { padding: 1rem 0.5rem; }
|
||||
.page-header { flex-direction:column; align-items:flex-start; gap:12px; }
|
||||
.page-header-right { flex-wrap: wrap; gap: 8px; }
|
||||
.g-card { padding: 1.2rem; }
|
||||
.act-item { gap: 10px; padding: 8px; }
|
||||
.hdr-chip { padding: 6px 12px; }
|
||||
}
|
||||
@media (max-width:480px) {
|
||||
.stats-grid { grid-template-columns:1fr; }
|
||||
.d-wrap { padding: 0.8rem 0; }
|
||||
.g-card { padding: 1rem; border-radius: 16px; }
|
||||
.act-meta { font-size: 10px; }
|
||||
.s-card { padding: 1.2rem; }
|
||||
.act-item { flex-direction: column; align-items: flex-start; gap: 8px; }
|
||||
.act-item .pill { align-self: flex-start; }
|
||||
}
|
||||
</style>
|
||||
|
||||
{{-- ═══════ MODAL (Detail Kegiatan) ═══════ --}}
|
||||
<div class="modal-overlay" id="detailModal">
|
||||
<div class="modal-box">
|
||||
<div class="modal-header">
|
||||
<h4 id="mdl-title">—</h4>
|
||||
<button class="modal-close" onclick="closeModal()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="mdl-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-wrap">
|
||||
|
||||
{{-- ── PAGE HEADER ── --}}
|
||||
<div class="page-header">
|
||||
<div class="page-header-left">
|
||||
<h2>Dashboard Admin</h2>
|
||||
<div class="subtitle">
|
||||
<span class="status-dot"></span>
|
||||
{{ \Carbon\Carbon::now()->timezone('Asia/Jakarta')->locale('id')->translatedFormat('l, d F Y') }}
|
||||
· Sistem Online
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-header-right">
|
||||
<span class="hdr-chip light">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
{{ \Carbon\Carbon::now()->timezone('Asia/Jakarta')->format('H:i') }} WIB
|
||||
</span>
|
||||
<span class="hdr-chip navy">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Disnaker Kalbar
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── STAT CARDS ── --}}
|
||||
<div class="stats-grid">
|
||||
|
||||
<div class="s-card c-blue">
|
||||
<div class="s-icon c-blue">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"/>
|
||||
<rect x="9" y="3" width="6" height="4" rx="1"/>
|
||||
<line x1="9" y1="12" x2="15" y2="12"/>
|
||||
<line x1="9" y1="16" x2="13" y2="16"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="s-num">{{ $stats['total'] ?? 0 }}</div>
|
||||
<div class="s-label">Total Kegiatan</div>
|
||||
<div class="s-trend up">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>
|
||||
12% bulan ini
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="s-card c-green">
|
||||
<div class="s-icon c-green">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
||||
<polyline points="22 4 12 14.01 9 11.01"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="s-num">{{ $stats['selesai'] ?? 0 }}</div>
|
||||
<div class="s-label">Kegiatan Selesai</div>
|
||||
<div class="s-trend up">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>
|
||||
8% dari kemarin
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="s-card c-yellow">
|
||||
<div class="s-icon c-yellow">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="12" y1="2" x2="12" y2="6"/>
|
||||
<line x1="12" y1="18" x2="12" y2="22"/>
|
||||
<line x1="4.93" y1="4.93" x2="7.76" y2="7.76"/>
|
||||
<line x1="16.24" y1="16.24" x2="19.07" y2="19.07"/>
|
||||
<line x1="2" y1="12" x2="6" y2="12"/>
|
||||
<line x1="18" y1="12" x2="22" y2="12"/>
|
||||
<line x1="4.93" y1="19.07" x2="7.76" y2="16.24"/>
|
||||
<line x1="16.24" y1="7.76" x2="19.07" y2="4.93"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="s-num">{{ $stats['proses'] ?? 0 }}</div>
|
||||
<div class="s-label">Sedang Berjalan</div>
|
||||
<div class="s-trend flat">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
Sama seperti kemarin
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="s-card c-red">
|
||||
<div class="s-icon c-red">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="s-num">{{ $stats['users'] ?? 0 }}</div>
|
||||
<div class="s-label">Pengguna Aktif</div>
|
||||
<div class="s-trend down">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
3% minggu ini
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- ── MAIN GRID ── --}}
|
||||
<div class="content-grid">
|
||||
|
||||
{{-- LEFT: Kegiatan Terbaru --}}
|
||||
<div class="g-card">
|
||||
<div class="g-card-title">
|
||||
<h3>5 Kegiatan Terbaru</h3>
|
||||
<a href="{{ route('admin.kegiatan.index') }}">Lihat Semua →</a>
|
||||
</div>
|
||||
|
||||
<div class="ftabs">
|
||||
<div class="ftab on" onclick="filterAct(this,'semua')">Semua</div>
|
||||
<div class="ftab off" onclick="filterAct(this,'selesai')">Selesai</div>
|
||||
<div class="ftab off" onclick="filterAct(this,'on_progres')">Berjalan</div>
|
||||
<div class="ftab off" onclick="filterAct(this,'terlambat')">Terlambat</div>
|
||||
<div class="ftab off" onclick="filterAct(this,'dibatalkan')">Dibatalkan</div>
|
||||
</div>
|
||||
|
||||
<div class="act-divider"></div>
|
||||
|
||||
<div id="act-list">
|
||||
@php
|
||||
$avClasses = ['av-1','av-2','av-3','av-4','av-5'];
|
||||
$pillMap = [
|
||||
'selesai' => ['pill-ok', 'Selesai'],
|
||||
'telah_ditinjau' => ['pill-ok', 'Ditinjau'],
|
||||
'on_progres' => ['pill-run', 'Berjalan'],
|
||||
'terlambat' => ['pill-del', 'Terlambat'],
|
||||
'dibatalkan' => ['pill-del', 'Dibatalkan'],
|
||||
'review' => ['pill-pnd', 'Review'],
|
||||
'pending' => ['pill-pnd', 'Pending'],
|
||||
];
|
||||
@endphp
|
||||
|
||||
@forelse($activities ?? [] as $idx => $item)
|
||||
@php
|
||||
$st = strtolower($item->status ?? 'pending');
|
||||
$pc = $pillMap[$st] ?? ['pill-pnd', ucfirst($st)];
|
||||
$ac = $avClasses[$idx % 5];
|
||||
$tgl = \Carbon\Carbon::parse($item->tanggal)->locale('id')->translatedFormat('d M Y');
|
||||
$usr = e($item->user->name ?? 'Sistem');
|
||||
$nm = e($item->nama_kegiatan);
|
||||
@endphp
|
||||
<div class="act-item"
|
||||
data-status="{{ $st }}"
|
||||
onclick="openModal(
|
||||
'{{ addslashes($nm) }}',
|
||||
'{{ addslashes($usr) }}',
|
||||
'{{ $tgl }}',
|
||||
'{{ $st }}',
|
||||
'{{ addslashes(str_replace(["\r", "\n"], ["", "\\n"], $item->keterangan ?? '-')) }}',
|
||||
'{{ addslashes(str_replace(["\r", "\n"], ["", "\\n"], $item->status_note ?? '')) }}',
|
||||
'{{ addslashes($item->lokasi ?? '-') }}',
|
||||
'{{ $item->lampiran ? rtrim(env('BACKEND_API_URL', 'http://127.0.0.1:8000'), '/api') . '/storage/' . $item->lampiran : '' }}'
|
||||
)">
|
||||
<div class="act-ava {{ $ac }}">{{ strtoupper(substr($item->user->name ?? 'U', 0, 2)) }}</div>
|
||||
<div class="act-body">
|
||||
<div class="act-name">{{ $nm }}</div>
|
||||
<div class="act-meta">{{ $usr }} • {{ $tgl }}</div>
|
||||
</div>
|
||||
<span class="pill {{ $pc[0] }}">{{ $pc[1] }}</span>
|
||||
</div>
|
||||
@empty
|
||||
<div class="empty-box">
|
||||
<div class="ei">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>
|
||||
</div>
|
||||
<p>Belum ada aktivitas kegiatan yang tercatat.</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- RIGHT: Sidebar --}}
|
||||
<div class="sidebar-col">
|
||||
|
||||
{{-- Weekly Chart --}}
|
||||
<div class="g-card">
|
||||
<div class="g-card-title" style="margin-bottom:.8rem">
|
||||
<h3>Aktivitas Minggu Ini</h3>
|
||||
@php $wTotal = array_sum($weeklyData ?? [0,0,0,0,0,0,0]); @endphp
|
||||
<span class="pill pill-run">{{ $wTotal }} kegiatan</span>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$days = ['Sen','Sel','Rab','Kam','Jum','Sab','Min'];
|
||||
$wData = $weeklyData ?? [0,0,0,0,0,0,0];
|
||||
$maxBar = max($wData) ?: 1;
|
||||
$todayDI = (int)\Carbon\Carbon::now()->dayOfWeekIso - 1;
|
||||
@endphp
|
||||
|
||||
<div class="chart-bars">
|
||||
@foreach($days as $i => $d)
|
||||
@php $pct = round(($wData[$i] / $maxBar) * 100); $isToday = $i === $todayDI; @endphp
|
||||
<div class="bar-wrap">
|
||||
<div class="bar-body {{ $isToday ? 'today' : ($wData[$i] > 0 ? 'active' : '') }}"
|
||||
style="height:{{ max($pct, 8) }}%"
|
||||
data-val="{{ $wData[$i] }} kegiatan">
|
||||
</div>
|
||||
<span class="bar-lbl {{ $isToday ? 'today-lbl' : '' }}">{{ $d }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="chart-footer">
|
||||
<span>Total minggu ini</span>
|
||||
<strong>{{ $wTotal }} kegiatan</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Division Progress --}}
|
||||
<div class="g-card">
|
||||
<div class="g-card-title" style="margin-bottom:.8rem">
|
||||
<h3>Progres Divisi</h3>
|
||||
</div>
|
||||
{{-- FIX: Gunakan $divisionProgress dari controller, fallback jika tidak ada --}}
|
||||
@php
|
||||
$divs = $divisionProgress ?? [
|
||||
['name' => 'Operasional', 'pct' => 78, 'clr' => 'linear-gradient(90deg,#3b82f6,#6366f1)'],
|
||||
['name' => 'Keuangan', 'pct' => 64, 'clr' => 'linear-gradient(90deg,#22c55e,#10b981)'],
|
||||
['name' => 'SDM', 'pct' => 91, 'clr' => 'linear-gradient(90deg,#f97316,#f59e0b)'],
|
||||
['name' => 'IT', 'pct' => 55, 'clr' => 'linear-gradient(90deg,#a855f7,#ec4899)'],
|
||||
];
|
||||
@endphp
|
||||
@foreach($divs as $dv)
|
||||
<div class="prog-item">
|
||||
<div class="prog-head">
|
||||
<span>{{ $dv['name'] }}</span>
|
||||
<strong>{{ $dv['pct'] ?? 0 }}%</strong>
|
||||
</div>
|
||||
<div class="prog-track">
|
||||
<div class="prog-fill" style="width:{{ $dv['pct'] ?? 0 }}%;background:{{ $dv['clr'] ?? '#3b82f6' }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- Top Kontributor --}}
|
||||
@if(!empty($topUsers) && count($topUsers))
|
||||
<div class="g-card">
|
||||
<div class="g-card-title" style="margin-bottom:.8rem">
|
||||
<h3>Top Kontributor</h3>
|
||||
</div>
|
||||
@foreach($topUsers as $ri => $u)
|
||||
@php
|
||||
$rankCls = match($ri) { 0 => 'rank-1', 1 => 'rank-2', 2 => 'rank-3', default => 'rank-n' };
|
||||
$medal = match($ri) {
|
||||
0 => '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="#b45309" stroke="none"><circle cx="12" cy="8" r="6"/><path d="M15.477 12.89L17 22l-5-3-5 3 1.523-9.11"/></svg>',
|
||||
1 => '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="#64748b" stroke="none"><circle cx="12" cy="8" r="6"/><path d="M15.477 12.89L17 22l-5-3-5 3 1.523-9.11"/></svg>',
|
||||
2 => '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="#c2410c" stroke="none"><circle cx="12" cy="8" r="6"/><path d="M15.477 12.89L17 22l-5-3-5 3 1.523-9.11"/></svg>',
|
||||
default => $ri + 1
|
||||
};
|
||||
@endphp
|
||||
<div class="usr-row">
|
||||
<div class="usr-rank {{ $rankCls }}">{!! $medal !!}</div>
|
||||
<div class="usr-ava">{{ strtoupper(substr($u->name, 0, 2)) }}</div>
|
||||
<span class="usr-name">{{ $u->name }}</span>
|
||||
{{-- FIX: Ganti kegiatan_count → activities_count (sesuai withCount('activities')) --}}
|
||||
<span class="usr-count">{{ $u->activities_count ?? 0 }} Lap.</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>{{-- /sidebar --}}
|
||||
</div>{{-- /content-grid --}}
|
||||
|
||||
</div>{{-- /d-wrap --}}
|
||||
|
||||
<script>
|
||||
/* ── Filter Tabs ─────────────────── */
|
||||
function filterAct(el, status) {
|
||||
document.querySelectorAll('.ftab').forEach(t => {
|
||||
t.classList.remove('on');
|
||||
t.classList.add('off');
|
||||
});
|
||||
el.classList.add('on');
|
||||
el.classList.remove('off');
|
||||
document.querySelectorAll('#act-list .act-item').forEach(row => {
|
||||
row.style.display = (status === 'semua' || row.dataset.status === status) ? 'flex' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Modal ───────────────────────── */
|
||||
const pillLabels = {
|
||||
selesai: '<span class="pill pill-ok">Selesai</span>',
|
||||
on_progres: '<span class="pill pill-run">Berjalan</span>',
|
||||
terlambat: '<span class="pill pill-del">Terlambat</span>',
|
||||
dibatalkan: '<span class="pill pill-del">Dibatalkan</span>',
|
||||
review: '<span class="pill pill-pnd">Review</span>',
|
||||
pending: '<span class="pill pill-pnd">Pending</span>'
|
||||
};
|
||||
|
||||
function openModal(nama, user, tgl, status, ket, statusNote, lokasi, lampiranUrl) {
|
||||
document.getElementById('mdl-title').textContent = nama;
|
||||
|
||||
let lampiranHtml = '';
|
||||
if (lampiranUrl) {
|
||||
lampiranHtml = `
|
||||
<div class="modal-field" style="margin-top: 15px; grid-column: span 2;">
|
||||
<span class="mf-label">Lampiran Foto/Dokumen</span>
|
||||
<div style="margin-top: 8px;">
|
||||
<img src="${lampiranUrl}" alt="Lampiran" style="max-width: 100%; max-height: 400px; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
document.getElementById('mdl-body').innerHTML = `
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
|
||||
<div class="modal-field">
|
||||
<span class="mf-label">Pegawai Ditugaskan</span>
|
||||
<span class="mf-value">${user}</span>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<span class="mf-label">Tanggal</span>
|
||||
<span class="mf-value">${tgl}</span>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<span class="mf-label">Lokasi Tugas</span>
|
||||
<span class="mf-value">${lokasi}</span>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<span class="mf-label">Status</span>
|
||||
<span class="mf-value">${pillLabels[status] || status}</span>
|
||||
</div>
|
||||
<div class="modal-field" style="grid-column: span 2;">
|
||||
<span class="mf-label">Deskripsi Laporan (Keterangan)</span>
|
||||
<span class="mf-value" style="white-space: pre-wrap; line-height: 1.5;">${ket || '—'}</span>
|
||||
</div>
|
||||
${statusNote ? `
|
||||
<div class="modal-field" style="grid-column: span 2; background: #eff6ff; padding: 12px; border-radius: 8px;">
|
||||
<span class="mf-label" style="color: #2563eb; margin-bottom: 4px; display: block;">Catatan Status (Node Petugas)</span>
|
||||
<span class="mf-value" style="white-space: pre-wrap; line-height: 1.5; color: #1e3a8a;">${statusNote}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${lampiranHtml}
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('detailModal').classList.add('show');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('detailModal').classList.remove('show');
|
||||
}
|
||||
|
||||
document.getElementById('detailModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeModal();
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') closeModal();
|
||||
});
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,212 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('content')
|
||||
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap');
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
body, * { font-family: 'Plus Jakarta Sans', sans-serif; }
|
||||
|
||||
.eu-wrap {
|
||||
padding: 2rem 2.5rem 3rem;
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.eu-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.eu-title { font-size: 22px; font-weight: 700; color: #0D215C; }
|
||||
.eu-sub { font-size: 13px; color: #94a3b8; margin-top: 4px; }
|
||||
.btn-back {
|
||||
padding: 8px 18px;
|
||||
border-radius: 10px;
|
||||
background: #eef2ff;
|
||||
color: #3730a3;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: .2s;
|
||||
}
|
||||
.btn-back:hover { background: #dbeafe; color: #1d4ed8; }
|
||||
|
||||
.eu-card {
|
||||
background: #fff;
|
||||
border: 1px solid #edf1f7;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgba(13,33,92,.05);
|
||||
}
|
||||
.eu-card-header {
|
||||
padding: 1.2rem 1.5rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
background: #f8fafc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.eu-card-header h3 { font-size: 14px; font-weight: 700; color: #0D215C; margin: 0; }
|
||||
.eu-body { padding: 1.5rem; }
|
||||
|
||||
.form-group { margin-bottom: 1.2rem; }
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #374151;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
.form-input, .form-select-field {
|
||||
width: 100%;
|
||||
padding: 11px 14px;
|
||||
border: 1.5px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
color: #0f172a;
|
||||
background: #fff;
|
||||
transition: border .2s, box-shadow .2s;
|
||||
}
|
||||
.form-input:focus, .form-select-field:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59,130,246,.1);
|
||||
}
|
||||
.form-hint {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.pw-wrap { position: relative; }
|
||||
.pw-wrap .form-input { padding-right: 44px; }
|
||||
.pw-toggle {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.pw-toggle:hover { color: #3b82f6; }
|
||||
|
||||
.btn-submit {
|
||||
width: 100%;
|
||||
padding: 13px;
|
||||
margin-top: .5rem;
|
||||
background: #0D215C;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: .2s;
|
||||
box-shadow: 0 4px 14px rgba(13,33,92,.2);
|
||||
}
|
||||
.btn-submit:hover { background: #1e3a8a; transform: translateY(-1px); }
|
||||
|
||||
.alert-error {
|
||||
background: #fee2e2;
|
||||
color: #b91c1c;
|
||||
padding: 12px 16px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="eu-wrap">
|
||||
<div class="eu-header">
|
||||
<div>
|
||||
<div class="eu-title">Edit Pengguna</div>
|
||||
<div class="eu-sub">Perbarui informasi akun pengguna</div>
|
||||
</div>
|
||||
<a href="{{ route('admin.users') }}" class="btn-back">← Kembali</a>
|
||||
</div>
|
||||
|
||||
<div class="eu-card">
|
||||
<div class="eu-card-header">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#0D215C" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
<h3>Form Edit Pengguna</h3>
|
||||
</div>
|
||||
<div class="eu-body">
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert-error">
|
||||
@foreach($errors->all() as $e) <div>• {{ $e }}</div> @endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<div class="alert-error">✗ {{ session('error') }}</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('admin.users.update', $user->id) }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="name">Nama Lengkap</label>
|
||||
<input type="text" id="name" name="name" class="form-input"
|
||||
value="{{ old('name', $user->name) }}" placeholder="Nama lengkap pengguna" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="username">Username</label>
|
||||
<input type="text" id="username" name="username" class="form-input"
|
||||
value="{{ old('username', $user->username) }}" placeholder="Username login" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="jabatan">Jabatan</label>
|
||||
<input type="text" id="jabatan" name="jabatan" class="form-input"
|
||||
value="{{ old('jabatan', $user->jabatan ?? '') }}" placeholder="Jabatan pengguna (opsional)">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="role">Role / Hak Akses</label>
|
||||
<select id="role" name="role" class="form-select-field">
|
||||
<option value="user" {{ old('role', $user->role) == 'user' ? 'selected' : '' }}>Petugas Lapangan</option>
|
||||
<option value="admin" {{ old('role', $user->role) == 'admin' ? 'selected' : '' }}>Administrator (Kasi)</option>
|
||||
<option value="superadmin" {{ old('role', $user->role) == 'superadmin' ? 'selected' : '' }}>Super Admin (Kabid)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password">Password Baru</label>
|
||||
<div class="pw-wrap">
|
||||
<input type="password" id="password" name="password" class="form-input"
|
||||
placeholder="Kosongkan jika tidak ingin mengubah">
|
||||
<button type="button" class="pw-toggle" onclick="togglePassword('password','eyeIconPw')">
|
||||
<svg id="eyeIconPw" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-hint">Minimal 6 karakter. Kosongkan jika tidak ingin mengubah password.</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-submit">Simpan Perubahan</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function togglePassword(fieldId, iconId) {
|
||||
const field = document.getElementById(fieldId);
|
||||
const icon = document.getElementById(iconId);
|
||||
if (field.type === 'password') {
|
||||
field.type = 'text';
|
||||
icon.innerHTML = '<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/>';
|
||||
} else {
|
||||
field.type = 'password';
|
||||
icon.innerHTML = '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,242 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('content')
|
||||
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap');
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
body, * { font-family: 'Plus Jakarta Sans', sans-serif; }
|
||||
|
||||
.detail-wrap {
|
||||
padding: 2rem 2.5rem 3rem;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.detail-title { font-size: 22px; font-weight: 700; color: #0D215C; }
|
||||
.detail-sub { font-size: 13px; color: #94a3b8; margin-top: 4px; }
|
||||
|
||||
.btn-back {
|
||||
padding: 8px 18px;
|
||||
border-radius: 10px;
|
||||
background: #eef2ff;
|
||||
color: #3730a3;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: all .2s;
|
||||
}
|
||||
.btn-back:hover { background: #dbeafe; color: #1d4ed8; }
|
||||
|
||||
.detail-card {
|
||||
background: #fff;
|
||||
border: 1px solid #edf1f7;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgba(13,33,92,.05);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.detail-card-header {
|
||||
padding: 1.2rem 1.5rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
background: #f8fafc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.detail-card-header h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #0D215C;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0;
|
||||
}
|
||||
.detail-field {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.detail-field:nth-child(odd) { border-right: 1px solid #f1f5f9; }
|
||||
.detail-field.full {
|
||||
grid-column: 1 / -1;
|
||||
border-right: none;
|
||||
}
|
||||
.df-label {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #94a3b8;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .5px;
|
||||
}
|
||||
.df-value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 5px 14px;
|
||||
border-radius: 50px;
|
||||
letter-spacing: .3px;
|
||||
}
|
||||
.pill-ok { background: #dcfce7; color: #15803d; }
|
||||
.pill-run { background: #dbeafe; color: #1d4ed8; }
|
||||
.pill-rev { background: #fef9c3; color: #92400e; }
|
||||
.pill-del { background: #fee2e2; color: #b91c1c; }
|
||||
|
||||
.petugas-list {
|
||||
padding: 1rem 1.5rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
.petugas-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 10px;
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #334155;
|
||||
}
|
||||
.petugas-ava {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #0D215C, #2563eb);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.action-btn {
|
||||
padding: 10px 22px;
|
||||
border-radius: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: .2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.btn-edit { background: #0D215C; color: #fff; box-shadow: 0 4px 12px rgba(13,33,92,.2); }
|
||||
.btn-edit:hover { background: #1e3a8a; }
|
||||
.btn-batal { background: #fee2e2; color: #b91c1c; }
|
||||
.btn-batal:hover { background: #fecaca; }
|
||||
</style>
|
||||
|
||||
<div class="detail-wrap">
|
||||
|
||||
{{-- HEADER --}}
|
||||
<div class="detail-header">
|
||||
<div>
|
||||
<div class="detail-title">Detail Kegiatan</div>
|
||||
<div class="detail-sub">Informasi lengkap penugasan lapangan</div>
|
||||
</div>
|
||||
<a href="{{ route('admin.kegiatan.index') }}" class="btn-back">← Kembali</a>
|
||||
</div>
|
||||
|
||||
{{-- INFO UTAMA --}}
|
||||
<div class="detail-card">
|
||||
<div class="detail-card-header">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#0D215C" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"/><rect x="9" y="3" width="6" height="4" rx="1"/></svg>
|
||||
<h3>Informasi Penugasan</h3>
|
||||
</div>
|
||||
|
||||
<div class="detail-grid">
|
||||
<div class="detail-field">
|
||||
<div class="df-label">Nomor Surat</div>
|
||||
<div class="df-value">{{ $activity->nomor_surat ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="detail-field">
|
||||
<div class="df-label">Status</div>
|
||||
<div class="df-value">
|
||||
@php
|
||||
$pillMap = [
|
||||
'selesai' => ['pill-ok', 'Selesai'],
|
||||
'on_progres' => ['pill-run', 'Sedang Berjalan'],
|
||||
'telah_ditinjau' => ['pill-rev', 'Telah Ditinjau'],
|
||||
'dibatalkan' => ['pill-del', 'Dibatalkan'],
|
||||
];
|
||||
$pc = $pillMap[$activity->status] ?? ['pill-run', ucfirst($activity->status)];
|
||||
@endphp
|
||||
<span class="pill {{ $pc[0] }}">{{ $pc[1] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-field full">
|
||||
<div class="df-label">Nama Kegiatan</div>
|
||||
<div class="df-value">{{ $activity->nama_kegiatan }}</div>
|
||||
</div>
|
||||
<div class="detail-field">
|
||||
<div class="df-label">Tanggal Mulai</div>
|
||||
<div class="df-value">{{ \Carbon\Carbon::parse($activity->tanggal)->locale('id')->translatedFormat('d F Y') }}</div>
|
||||
</div>
|
||||
<div class="detail-field">
|
||||
<div class="df-label">Tanggal Selesai</div>
|
||||
<div class="df-value">
|
||||
{{ $activity->tanggal_selesai ? \Carbon\Carbon::parse($activity->tanggal_selesai)->locale('id')->translatedFormat('d F Y') : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-field">
|
||||
<div class="df-label">Penanggung Jawab</div>
|
||||
<div class="df-value">{{ optional($activity->user)->name ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="detail-field">
|
||||
<div class="df-label">Terakhir Diperbarui</div>
|
||||
<div class="df-value">{{ \Carbon\Carbon::parse($activity->updated_at)->locale('id')->translatedFormat('d F Y, H:i') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- DAFTAR PETUGAS --}}
|
||||
<div class="detail-card">
|
||||
<div class="detail-card-header">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#0D215C" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
<h3>Petugas yang Ditugaskan</h3>
|
||||
</div>
|
||||
<div class="petugas-list">
|
||||
@forelse($activity->petugas ?? [] as $p)
|
||||
@php $nm = is_array($p) ? ($p['name'] ?? '-') : ($p->name ?? '-'); @endphp
|
||||
<div class="petugas-chip">
|
||||
<div class="petugas-ava">{{ strtoupper(substr($nm, 0, 2)) }}</div>
|
||||
{{ $nm }}
|
||||
</div>
|
||||
@empty
|
||||
<span style="color:#94a3b8;font-size:13px;">Belum ada petugas yang ditetapkan.</span>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,220 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('content')
|
||||
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap');
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
body, * { font-family: 'Plus Jakarta Sans', sans-serif; }
|
||||
|
||||
.edit-wrap {
|
||||
padding: 2rem 2.5rem 3rem;
|
||||
max-width: 680px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.edit-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.edit-title { font-size: 22px; font-weight: 700; color: #0D215C; }
|
||||
.edit-sub { font-size: 13px; color: #94a3b8; margin-top: 4px; }
|
||||
|
||||
.btn-back {
|
||||
padding: 8px 18px;
|
||||
border-radius: 10px;
|
||||
background: #eef2ff;
|
||||
color: #3730a3;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: all .2s;
|
||||
}
|
||||
.btn-back:hover { background: #dbeafe; color: #1d4ed8; }
|
||||
|
||||
.edit-card {
|
||||
background: #fff;
|
||||
border: 1px solid #edf1f7;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgba(13,33,92,.05);
|
||||
}
|
||||
.edit-card-header {
|
||||
padding: 1.2rem 1.5rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
background: #f8fafc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.edit-card-header h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #0D215C;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.edit-body { padding: 1.5rem; }
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.info-row:last-of-type { border-bottom: none; }
|
||||
.info-label {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #94a3b8;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .5px;
|
||||
}
|
||||
.info-value {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.form-group { margin-top: 1.5rem; }
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #374151;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.form-select {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 1.5px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
color: #0f172a;
|
||||
transition: border .2s;
|
||||
appearance: auto;
|
||||
}
|
||||
.form-select:focus { border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59,130,246,.1); }
|
||||
|
||||
.status-info {
|
||||
margin-top: 10px;
|
||||
padding: 10px 14px;
|
||||
background: #fffbeb;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
color: #92400e;
|
||||
border: 1px solid #fde68a;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
margin-top: 1.5rem;
|
||||
background: #0D215C;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: .2s;
|
||||
box-shadow: 0 4px 14px rgba(13,33,92,.2);
|
||||
}
|
||||
.btn-submit:hover { background: #1e3a8a; transform: translateY(-1px); }
|
||||
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 4px 12px;
|
||||
border-radius: 50px;
|
||||
}
|
||||
.pill-ok { background: #dcfce7; color: #15803d; }
|
||||
.pill-run { background: #dbeafe; color: #1d4ed8; }
|
||||
.pill-rev { background: #fef9c3; color: #92400e; }
|
||||
.pill-del { background: #fee2e2; color: #b91c1c; }
|
||||
</style>
|
||||
|
||||
<div class="edit-wrap">
|
||||
|
||||
{{-- HEADER --}}
|
||||
<div class="edit-header">
|
||||
<div>
|
||||
<div class="edit-title">Ubah Status Kegiatan</div>
|
||||
<div class="edit-sub">Perbarui status kemajuan kegiatan lapangan</div>
|
||||
</div>
|
||||
<a href="{{ route('admin.kegiatan.show', $activity->id) }}" class="btn-back">← Kembali</a>
|
||||
</div>
|
||||
|
||||
{{-- FORM CARD --}}
|
||||
<div class="edit-card">
|
||||
<div class="edit-card-header">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#0D215C" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
||||
<h3>Form Edit Status</h3>
|
||||
</div>
|
||||
|
||||
<div class="edit-body">
|
||||
{{-- ALERT --}}
|
||||
@if($errors->any())
|
||||
<div style="background:#fee2e2;color:#b91c1c;padding:12px 16px;border-radius:10px;margin-bottom:1rem;font-size:13px;">
|
||||
@foreach($errors->all() as $e) <div>• {{ $e }}</div> @endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- INFO KEGIATAN --}}
|
||||
<div class="info-row">
|
||||
<div class="info-label">Nomor Surat</div>
|
||||
<div class="info-value">{{ $activity->nomor_surat ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">Nama Kegiatan</div>
|
||||
<div class="info-value">{{ $activity->nama_kegiatan }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">Status Saat Ini</div>
|
||||
<div class="info-value">
|
||||
@php
|
||||
$pillMap = [
|
||||
'selesai' => ['pill-ok', 'Selesai'],
|
||||
'on_progres' => ['pill-run', 'Sedang Berjalan'],
|
||||
'telah_ditinjau' => ['pill-rev', 'Telah Ditinjau'],
|
||||
'dibatalkan' => ['pill-del', 'Dibatalkan'],
|
||||
];
|
||||
$pc = $pillMap[$activity->status] ?? ['pill-run', ucfirst($activity->status)];
|
||||
@endphp
|
||||
<span class="pill {{ $pc[0] }}">{{ $pc[1] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- FORM --}}
|
||||
<form action="{{ route('admin.kegiatan.update', $activity->id) }}" method="POST">
|
||||
@csrf
|
||||
@method('POST')
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="status">Status Baru</label>
|
||||
<select name="status" id="status" class="form-select">
|
||||
<option value="on_progres" {{ $activity->status == 'on_progres' ? 'selected' : '' }}>Sedang Berjalan</option>
|
||||
<option value="selesai" {{ $activity->status == 'selesai' ? 'selected' : '' }}>Selesai</option>
|
||||
<option value="telah_ditinjau" {{ $activity->status == 'telah_ditinjau' ? 'selected' : '' }}>Telah Ditinjau</option>
|
||||
</select>
|
||||
<div class="status-info">
|
||||
⚠️ Perubahan status bersifat permanen dan akan tercatat dalam riwayat sistem.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-submit">Simpan Perubahan Status</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,329 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('content')
|
||||
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap');
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
.keg-wrap {
|
||||
padding: 2rem 2.5rem 3rem;
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
}
|
||||
|
||||
/* ── Header ─── */
|
||||
.keg-topbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
.keg-title { font-size: 22px; font-weight: 700; color: #0D215C; }
|
||||
.keg-sub { font-size: 12.5px; color: #94a3b8; margin-top: 4px; }
|
||||
|
||||
.keg-btn-back {
|
||||
padding: 8px 18px;
|
||||
border-radius: 10px;
|
||||
background: #eef2ff;
|
||||
color: #3730a3;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: all .2s;
|
||||
}
|
||||
.keg-btn-back:hover { background: #dbeafe; color: #1d4ed8; }
|
||||
|
||||
/* ── Stats Strip ─── */
|
||||
.keg-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3,1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.kstat {
|
||||
background: #fff;
|
||||
border: 1px solid #edf1f7;
|
||||
border-radius: 16px;
|
||||
padding: 1.2rem 1.4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
.kstat-icon {
|
||||
width: 42px; height: 42px;
|
||||
border-radius: 12px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.kstat-icon.blue { background: #eff6ff; color: #3b82f6; }
|
||||
.kstat-icon.green { background: #f0fdf4; color: #16a34a; }
|
||||
.kstat-icon.yellow { background: #fffbeb; color: #d97706; }
|
||||
.kstat-num { font-size: 28px; font-weight: 700; color: #0f172a; line-height: 1; }
|
||||
.kstat-label { font-size: 12px; color: #94a3b8; margin-top: 3px; font-weight: 500; }
|
||||
|
||||
/* ── Search + Filter ─── */
|
||||
.keg-filter-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 1.2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.keg-search {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
padding: 9px 14px;
|
||||
border: 1.5px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
transition: border .2s;
|
||||
}
|
||||
.keg-search:focus { border-color: #3b82f6; }
|
||||
.keg-select {
|
||||
padding: 9px 14px;
|
||||
border: 1.5px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.keg-filter-btn {
|
||||
padding: 9px 20px;
|
||||
background: #0D215C;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: .2s;
|
||||
}
|
||||
.keg-filter-btn:hover { background: #1e3a8a; }
|
||||
|
||||
/* ── Table Card ─── */
|
||||
.keg-card {
|
||||
background: #fff;
|
||||
border: 1px solid #edf1f7;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgba(13,33,92,.04);
|
||||
}
|
||||
.keg-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
.keg-table thead { background: #f8fafc; }
|
||||
.keg-table thead th {
|
||||
padding: 14px 18px;
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #94a3b8;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .5px;
|
||||
}
|
||||
.keg-table tbody td {
|
||||
padding: 14px 18px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
vertical-align: middle;
|
||||
color: #334155;
|
||||
}
|
||||
.keg-table tbody tr:hover { background: #f8fafc; }
|
||||
|
||||
.pill {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 4px 12px;
|
||||
border-radius: 50px;
|
||||
letter-spacing: .3px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pill-ok { background: #dcfce7; color: #15803d; }
|
||||
.pill-run { background: #dbeafe; color: #1d4ed8; }
|
||||
.pill-rev { background: #fef9c3; color: #92400e; }
|
||||
.pill-del { background: #fee2e2; color: #b91c1c; }
|
||||
|
||||
.act-btn {
|
||||
padding: 6px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: .15s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.btn-view { background: #eff6ff; color: #2563eb; }
|
||||
.btn-view:hover { background: #dbeafe; }
|
||||
.btn-edit-k { background: #f0fdf4; color: #15803d; }
|
||||
.btn-edit-k:hover { background: #dcfce7; }
|
||||
.btn-del-k { background: #fff1f2; color: #be123c; }
|
||||
.btn-del-k:hover { background: #fee2e2; }
|
||||
|
||||
.pg-wrap { padding: 16px 18px; border-top: 1px solid #f1f5f9; }
|
||||
|
||||
.empty-row td {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ── Responsive ─── */
|
||||
@media (max-width: 768px) {
|
||||
.keg-wrap { padding: 1rem; }
|
||||
.keg-topbar { flex-direction: column; align-items: flex-start; gap: 12px; }
|
||||
.keg-stats { grid-template-columns: 1fr; gap: 12px; }
|
||||
.keg-filter-bar { flex-direction: column; align-items: stretch; }
|
||||
.keg-search, .keg-select, .keg-filter-btn { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="keg-wrap">
|
||||
|
||||
{{-- ALERTS --}}
|
||||
@if(session('success'))
|
||||
<div style="background:#dcfce7;color:#166534;padding:12px 18px;border-radius:12px;margin-bottom:1.2rem;font-size:13px;font-weight:600;">
|
||||
✓ {{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div style="background:#fee2e2;color:#b91c1c;padding:12px 18px;border-radius:12px;margin-bottom:1.2rem;font-size:13px;font-weight:600;">
|
||||
✗ {{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- TOPBAR --}}
|
||||
<div class="keg-topbar">
|
||||
<div>
|
||||
<div class="keg-title">Daftar Kegiatan</div>
|
||||
<div class="keg-sub">Monitor dan kelola seluruh kegiatan lapangan</div>
|
||||
</div>
|
||||
<a href="{{ route('admin.dashboard') }}" class="keg-btn-back">← Kembali ke Dashboard</a>
|
||||
</div>
|
||||
|
||||
{{-- STATS --}}
|
||||
@php
|
||||
$totalAll = $allItems->count();
|
||||
$totalSelesai = $allItems->where('status','selesai')->count();
|
||||
$totalProses = $allItems->where('status','on_progres')->count();
|
||||
@endphp
|
||||
<div class="keg-stats">
|
||||
<div class="kstat">
|
||||
<div class="kstat-icon blue">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"/><rect x="9" y="3" width="6" height="4" rx="1"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="kstat-num">{{ $totalAll }}</div>
|
||||
<div class="kstat-label">Total Kegiatan</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kstat">
|
||||
<div class="kstat-icon green">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="kstat-num">{{ $totalSelesai }}</div>
|
||||
<div class="kstat-label">Selesai</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kstat">
|
||||
<div class="kstat-icon yellow">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="kstat-num">{{ $totalProses }}</div>
|
||||
<div class="kstat-label">Sedang Berjalan</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- FILTER BAR --}}
|
||||
<form method="GET" action="{{ route('admin.kegiatan.index') }}" class="keg-filter-bar">
|
||||
<input type="text" name="search" class="keg-search" placeholder="Cari nama kegiatan / penanggung jawab..." value="{{ request('search') }}">
|
||||
<select name="status" class="keg-select">
|
||||
<option value="semua" {{ request('status','semua')=='semua' ? 'selected':'' }}>Semua Status</option>
|
||||
<option value="on_progres" {{ request('status')=='on_progres' ? 'selected':'' }}>Sedang Berjalan</option>
|
||||
<option value="selesai" {{ request('status')=='selesai' ? 'selected':'' }}>Selesai</option>
|
||||
<option value="telah_ditinjau" {{ request('status')=='telah_ditinjau' ? 'selected':'' }}>Telah Ditinjau</option>
|
||||
</select>
|
||||
<button type="submit" class="keg-filter-btn">Cari</button>
|
||||
@if(request('search') || request('status'))
|
||||
<a href="{{ route('admin.kegiatan.index') }}" class="keg-btn-back" style="padding:9px 16px;font-size:12px;">Reset</a>
|
||||
@endif
|
||||
</form>
|
||||
|
||||
{{-- TABLE --}}
|
||||
<div class="keg-card">
|
||||
<div style="overflow-x: auto; width: 100%;">
|
||||
<table class="keg-table" style="min-width: 700px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Nama Kegiatan</th>
|
||||
<th>Penanggung Jawab</th>
|
||||
<th>Tanggal</th>
|
||||
<th>Status</th>
|
||||
<th>Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($activities as $i => $item)
|
||||
@php
|
||||
$namaUser = optional($item->user)->name ?? 'User Tidak Ada';
|
||||
$inisial = collect(explode(' ', $namaUser))
|
||||
->take(2)
|
||||
->map(fn($w) => strtoupper(substr($w,0,1)))
|
||||
->join('');
|
||||
$pillMap = [
|
||||
'selesai' => ['pill-ok', 'Selesai'],
|
||||
'on_progres' => ['pill-run', 'Berjalan'],
|
||||
'telah_ditinjau' => ['pill-rev', 'Ditinjau'],
|
||||
'dibatalkan' => ['pill-del', 'Dibatalkan'],
|
||||
];
|
||||
$pc = $pillMap[$item->status] ?? ['pill-run', ucfirst($item->status)];
|
||||
@endphp
|
||||
<tr>
|
||||
<td>{{ $activities->firstItem() + $i }}</td>
|
||||
<td><strong>{{ $item->nama_kegiatan }}</strong></td>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="background:#e0e7ff;color:#3730a3;width:30px;height:30px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;">{{ $inisial }}</span>
|
||||
{{ $namaUser }}
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ \Carbon\Carbon::parse($item->tanggal)->format('d M Y') }}</td>
|
||||
<td><span class="pill {{ $pc[0] }}">{{ $pc[1] }}</span></td>
|
||||
<td>
|
||||
<div style="display:flex;gap:6px;">
|
||||
<a href="{{ route('admin.kegiatan.show', $item->id) }}" class="act-btn btn-view">Lihat</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr class="empty-row">
|
||||
<td colspan="6">
|
||||
<div style="display:flex;flex-direction:column;align-items:center;gap:8px;color:#94a3b8;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>
|
||||
Tidak ada data kegiatan yang ditemukan.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pg-wrap">{{ $activities->links() }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dashboard Admin – Disnaker Prov. Kalimantan Barat</title>
|
||||
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
background: #F2F5FB;
|
||||
color: #334155;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
main {
|
||||
min-height: 100vh;
|
||||
}
|
||||
::-webkit-scrollbar { width: 5px; }
|
||||
::-webkit-scrollbar-track { background: #f1f5f9; }
|
||||
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@include('admin.layouts.navbar')
|
||||
|
||||
<main class="pt-10 px-4">
|
||||
{{-- Yield inilah panggung untuk konten kamu --}}
|
||||
@yield('content')
|
||||
</main>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('[onclick^="return confirm"]').forEach(function(btn) {
|
||||
const match = btn.getAttribute('onclick').match(/confirm\('([^']+)'\)/);
|
||||
if (match) {
|
||||
const message = match[1];
|
||||
btn.removeAttribute('onclick');
|
||||
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const form = btn.closest('form');
|
||||
|
||||
Swal.fire({
|
||||
title: 'Konfirmasi',
|
||||
text: message,
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3b82f6',
|
||||
cancelButtonColor: '#64748b',
|
||||
confirmButtonText: 'Ya, Lanjutkan',
|
||||
cancelButtonText: 'Batal',
|
||||
reverseButtons: true,
|
||||
customClass: {
|
||||
confirmButton: 'rounded-xl',
|
||||
cancelButton: 'rounded-xl mr-2'
|
||||
}
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
if (form) {
|
||||
form.submit();
|
||||
} else if(btn.getAttribute('href')) {
|
||||
window.location.href = btn.getAttribute('href');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,146 @@
|
||||
@php
|
||||
$role = session('user')['role'] ?? '';
|
||||
$dashboardRoute = $role === 'superadmin' ? route('admin.superadmin.dashboard') : route('admin.dashboard');
|
||||
$usersRoute = $role === 'superadmin' ? route('admin.superadmin.user.index') : route('admin.users');
|
||||
$penugasanRoute = $role === 'superadmin' ? route('admin.superadmin.kegiatan.create') : route('admin.kegiatan.index');
|
||||
|
||||
$isDashboard = request()->routeIs('*.dashboard');
|
||||
$isUsers = request()->routeIs('*.user*') || request()->routeIs('*.users*');
|
||||
$isPenugasan = request()->routeIs('*.kegiatan.*');
|
||||
|
||||
$activeStyle = "bg-[#0D215C] text-white";
|
||||
$inactiveStyle = "text-slate-500 bg-transparent hover:bg-slate-100 hover:text-[#0D215C]";
|
||||
|
||||
$roleVal = session('user')['role'] ?? 'Administrator';
|
||||
if ($roleVal === 'admin') {
|
||||
$roleVal = 'Administrator';
|
||||
} elseif ($roleVal === 'superadmin') {
|
||||
$roleVal = 'Super Admin';
|
||||
} else {
|
||||
$roleVal = 'Petugas';
|
||||
}
|
||||
@endphp
|
||||
<nav class="bg-white border-b border-slate-200 shadow-[0_1px_12px_rgba(13,33,92,0.06)] sticky top-0 z-50">
|
||||
<div class="max-w-[1440px] mx-auto px-4 md:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-[66px]">
|
||||
{{-- Logo & Brand --}}
|
||||
<div class="flex items-center gap-2 md:gap-3">
|
||||
<img src="{{ asset('Kalimantan-Barat-Logo-Vector.png') }}"
|
||||
alt="Logo Kalbar"
|
||||
class="w-9 md:w-[46px] h-auto object-contain">
|
||||
<div class="border-l-2 border-slate-200 pl-2 md:pl-3">
|
||||
<p class="text-[11px] md:text-[13.5px] font-bold text-[#0D215C] tracking-tight leading-tight m-0">
|
||||
Dinas Tenaga Kerja dan Transmigrasi
|
||||
</p>
|
||||
<p class="text-[8px] md:text-[10px] font-medium text-slate-400 tracking-[1.8px] uppercase mt-0.5 m-0">
|
||||
Provinsi Kalimantan Barat
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hamburger Button (Mobile) -->
|
||||
<div class="flex md:hidden">
|
||||
<button type="button" id="admin-menu-btn" class="text-slate-500 hover:text-[#0D215C] focus:outline-none p-2 rounded-md hover:bg-slate-100 transition-colors">
|
||||
<i data-lucide="menu" class="w-6 h-6"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Nav + User (Desktop) --}}
|
||||
<div class="hidden md:flex items-center gap-2">
|
||||
<div class="flex items-center gap-1 mr-2">
|
||||
<a href="{{ $dashboardRoute }}" class="text-[11.5px] font-semibold tracking-[0.4px] px-[18px] py-2 rounded-lg flex items-center gap-[7px] transition-all duration-200 {{ $isDashboard ? $activeStyle : $inactiveStyle }}">
|
||||
<i data-lucide="layout-dashboard" class="w-3.5 h-3.5"></i> Dashboard
|
||||
</a>
|
||||
<a href="{{ $usersRoute }}" class="text-[11.5px] font-semibold tracking-[0.4px] px-[18px] py-2 rounded-lg flex items-center gap-[7px] transition-all duration-200 {{ $isUsers ? $activeStyle : $inactiveStyle }}">
|
||||
<i data-lucide="users" class="w-3.5 h-3.5"></i> Kelola Pengguna
|
||||
</a>
|
||||
<a href="{{ $penugasanRoute }}" class="text-[11.5px] font-semibold tracking-[0.4px] px-[18px] py-2 rounded-lg flex items-center gap-[7px] transition-all duration-200 {{ $isPenugasan ? $activeStyle : $inactiveStyle }}">
|
||||
<i data-lucide="clipboard-list" class="w-3.5 h-3.5"></i> Penugasan
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="w-px h-7 bg-slate-200 mx-1"></div>
|
||||
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="text-right">
|
||||
<p class="text-[13px] font-semibold text-slate-900 leading-none m-0">
|
||||
{{ session('user')['name'] ?? 'User' }}
|
||||
</p>
|
||||
<p class="text-[10px] font-medium text-slate-400 uppercase tracking-[1.2px] mt-1 m-0">
|
||||
{{ $roleVal }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="w-9 h-9 rounded-lg bg-gradient-to-br from-[#0D215C] to-blue-800 text-white flex items-center justify-center font-bold text-xs tracking-[0.5px]">
|
||||
{{ strtoupper(substr(session('user')['name'] ?? 'US',0,2)) }}
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}" class="m-0">
|
||||
@csrf
|
||||
<button type="submit" class="w-9 h-9 rounded-lg bg-slate-50 text-slate-400 border border-slate-200 cursor-pointer flex items-center justify-center transition-all duration-200 hover:bg-rose-50 hover:text-rose-500 hover:border-rose-200" title="Keluar dari Sistem">
|
||||
<i data-lucide="log-out" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Panel -->
|
||||
<div id="admin-mobile-menu" class="hidden md:hidden bg-white border-t border-slate-100 shadow-xl absolute w-full left-0 pb-4">
|
||||
<div class="px-4 py-3 flex flex-col gap-2">
|
||||
<a href="{{ $dashboardRoute }}" class="text-[13px] font-semibold tracking-[0.4px] px-4 py-3 rounded-lg flex items-center gap-3 transition-all duration-200 {{ $isDashboard ? $activeStyle : $inactiveStyle }}">
|
||||
<i data-lucide="layout-dashboard" class="w-4 h-4"></i> Dashboard
|
||||
</a>
|
||||
<a href="{{ $usersRoute }}" class="text-[13px] font-semibold tracking-[0.4px] px-4 py-3 rounded-lg flex items-center gap-3 transition-all duration-200 {{ $isUsers ? $activeStyle : $inactiveStyle }}">
|
||||
<i data-lucide="users" class="w-4 h-4"></i> Kelola Pengguna
|
||||
</a>
|
||||
<a href="{{ $penugasanRoute }}" class="text-[13px] font-semibold tracking-[0.4px] px-4 py-3 rounded-lg flex items-center gap-3 transition-all duration-200 {{ $isPenugasan ? $activeStyle : $inactiveStyle }}">
|
||||
<i data-lucide="clipboard-list" class="w-4 h-4"></i> Penugasan
|
||||
</a>
|
||||
|
||||
<div class="h-px w-full bg-slate-100 my-1"></div>
|
||||
|
||||
<div class="flex items-center justify-between px-2 py-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-lg bg-gradient-to-br from-[#0D215C] to-blue-800 text-white flex items-center justify-center font-bold text-xs tracking-[0.5px]">
|
||||
{{ strtoupper(substr(session('user')['name'] ?? 'US',0,2)) }}
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[13px] font-semibold text-slate-900 leading-none m-0">
|
||||
{{ session('user')['name'] ?? 'User' }}
|
||||
</p>
|
||||
<p class="text-[10px] font-medium text-slate-400 uppercase tracking-[1.2px] mt-1 m-0">
|
||||
{{ $roleVal }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="{{ route('logout') }}" class="m-0">
|
||||
@csrf
|
||||
<button type="submit" class="w-9 h-9 rounded-lg bg-slate-50 text-slate-400 border border-slate-200 cursor-pointer flex items-center justify-center transition-all duration-200 hover:bg-rose-50 hover:text-rose-500 hover:border-rose-200" title="Keluar dari Sistem">
|
||||
<i data-lucide="log-out" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const btn = document.getElementById('admin-menu-btn');
|
||||
const menu = document.getElementById('admin-mobile-menu');
|
||||
if(btn && menu) {
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
menu.classList.toggle('hidden');
|
||||
lucide.createIcons();
|
||||
});
|
||||
document.addEventListener('click', function(e) {
|
||||
if(!menu.contains(e.target) && !btn.contains(e.target)) {
|
||||
menu.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,268 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('content')
|
||||
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap');
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
body, * { font-family: 'Plus Jakarta Sans', sans-serif; }
|
||||
|
||||
.user-wrap { padding: 2rem 2.5rem 3rem; max-width: 1200px; margin: 0 auto; }
|
||||
|
||||
/* ALERTS */
|
||||
.alert { padding: 12px 18px; border-radius: 12px; margin-bottom: 1.2rem; font-size: 13px; font-weight: 600; }
|
||||
.alert-success { background: #dcfce7; color: #166534; }
|
||||
.alert-error { background: #fee2e2; color: #b91c1c; }
|
||||
|
||||
/* HEADER */
|
||||
.user-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.user-title h2 { font-size: 26px; font-weight: 800; color: #0f172a; margin: 0; }
|
||||
.user-title p { font-size: 13px; color: #94a3b8; margin-top: 5px; }
|
||||
|
||||
.top-btn { display: flex; gap: 12px; }
|
||||
.btn-ui {
|
||||
border: none;
|
||||
padding: 10px 18px;
|
||||
border-radius: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
transition: .2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-dashboard { background: #eef2ff; color: #3730a3; }
|
||||
.btn-dashboard:hover { background: #dbeafe; transform: translateY(-2px); }
|
||||
.btn-add { background: #0D215C; color: #fff; box-shadow: 0 4px 14px rgba(13,33,92,.2); }
|
||||
.btn-add:hover { background: #1e3a8a; transform: translateY(-2px); }
|
||||
|
||||
/* STATS */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
padding: 1.2rem 1.4rem;
|
||||
border: 1px solid #edf1f7;
|
||||
transition: .25s;
|
||||
cursor: default;
|
||||
box-shadow: 0 2px 8px rgba(13,33,92,.04);
|
||||
}
|
||||
.stat-card:hover { transform: translateY(-4px); box-shadow: 0 12px 30px rgba(13,33,92,.1); }
|
||||
.stat-label { font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: .5px; margin-bottom: 8px; }
|
||||
.stat-number { font-size: 34px; font-weight: 800; color: #0f172a; }
|
||||
|
||||
/* TABLE */
|
||||
.user-card {
|
||||
background: #fff;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #edf1f7;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgba(13,33,92,.04);
|
||||
}
|
||||
.user-card-header {
|
||||
padding: 1.2rem 1.5rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.user-card-header h3 { font-size: 15px; font-weight: 700; color: #0D215C; margin: 0; }
|
||||
|
||||
.table-wrap { overflow-x: auto; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
thead { background: #f8fafc; }
|
||||
thead th {
|
||||
padding: 14px 18px;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
color: #94a3b8;
|
||||
text-align: left;
|
||||
font-weight: 700;
|
||||
letter-spacing: .5px;
|
||||
}
|
||||
tbody td {
|
||||
padding: 15px 18px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
vertical-align: middle;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
}
|
||||
tbody tr:hover { background: #f8fafc; }
|
||||
|
||||
.user-info { display: flex; align-items: center; gap: 12px; }
|
||||
.avatar {
|
||||
width: 40px; height: 40px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #0D215C, #2563eb);
|
||||
color: #fff;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.user-name { font-weight: 700; color: #0f172a; font-size: 14px; }
|
||||
.user-id { font-size: 11px; color: #94a3b8; margin-top: 2px; }
|
||||
|
||||
.role-badge {
|
||||
padding: 5px 14px;
|
||||
border-radius: 50px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .3px;
|
||||
}
|
||||
.role-superadmin { background: #fef3c7; color: #92400e; }
|
||||
.role-admin { background: #dbeafe; color: #1d4ed8; }
|
||||
.role-user { background: #dcfce7; color: #166534; }
|
||||
|
||||
.action-group { display: flex; gap: 8px; }
|
||||
.action-btn {
|
||||
border: none;
|
||||
padding: 7px 14px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: .15s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.btn-edit { background: #dbeafe; color: #2563eb; }
|
||||
.btn-edit:hover { background: #bfdbfe; }
|
||||
.btn-delete { background: #fee2e2; color: #dc2626; }
|
||||
.btn-delete:hover { background: #fecaca; }
|
||||
|
||||
/* RESPONSIVE */
|
||||
@media (max-width: 768px) {
|
||||
.user-wrap { padding: 1rem 0.5rem; }
|
||||
.user-header { flex-direction: column; align-items: flex-start; gap: 16px; margin-bottom: 1.5rem; }
|
||||
.top-btn { flex-wrap: wrap; width: 100%; }
|
||||
.top-btn .btn-ui { flex: 1; justify-content: center; text-align: center; }
|
||||
.stats-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; margin-bottom: 1.5rem; }
|
||||
.user-card-header { padding: 1rem; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.stats-grid { grid-template-columns: 1fr; }
|
||||
.user-title h2 { font-size: 22px; }
|
||||
.user-card { border-radius: 16px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="user-wrap">
|
||||
|
||||
{{-- ALERTS --}}
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success">✓ {{ session('success') }}</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div class="alert alert-error">✗ {{ session('error') }}</div>
|
||||
@endif
|
||||
|
||||
{{-- HEADER --}}
|
||||
<div class="user-header">
|
||||
<div class="user-title">
|
||||
<h2>Manajemen Pengguna</h2>
|
||||
<p>Kelola seluruh akun dan hak akses pengguna sistem</p>
|
||||
</div>
|
||||
<div class="top-btn">
|
||||
<a href="{{ route('admin.dashboard') }}" class="btn-ui btn-dashboard">← Dashboard</a>
|
||||
<a href="{{ route('admin.users.create') }}" class="btn-ui btn-add">+ Tambah Pengguna</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- STATS --}}
|
||||
@php
|
||||
$usersCol = collect($users);
|
||||
$totalUsers = $usersCol->count();
|
||||
$totalAdmin = $usersCol->where('role', 'admin')->count();
|
||||
$totalPetugas = $usersCol->where('role', 'user')->count();
|
||||
@endphp
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total Pengguna</div>
|
||||
<div class="stat-number">{{ $totalUsers }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Administrator</div>
|
||||
<div class="stat-number">{{ $totalAdmin }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Petugas Lapangan</div>
|
||||
<div class="stat-number">{{ $totalPetugas }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- TABLE --}}
|
||||
<div class="user-card">
|
||||
<div class="user-card-header">
|
||||
<h3>Daftar Pengguna Sistem</h3>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Pengguna</th>
|
||||
<th>Username</th>
|
||||
<th>Jabatan</th>
|
||||
<th>Role</th>
|
||||
<th>Terdaftar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($users as $user)
|
||||
@php $u = is_array($user) ? (object)$user : $user; @endphp
|
||||
<tr>
|
||||
<td>
|
||||
<div class="user-info">
|
||||
<div class="avatar">{{ strtoupper(substr($u->name ?? '-', 0, 2)) }}</div>
|
||||
<div>
|
||||
<div class="user-name">{{ $u->name ?? '-' }}</div>
|
||||
<div class="user-id">ID #{{ $u->id ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ $u->username ?? '-' }}</td>
|
||||
<td>{{ $u->jabatan ?? '-' }}</td>
|
||||
<td>
|
||||
@if(($u->role ?? '') == 'superadmin')
|
||||
<span class="role-badge role-superadmin">Super Admin</span>
|
||||
@elseif(($u->role ?? '') == 'admin')
|
||||
<span class="role-badge role-admin">Administrator</span>
|
||||
@else
|
||||
<span class="role-badge role-user">Petugas</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if(!empty($u->created_at))
|
||||
{{ \Carbon\Carbon::parse($u->created_at)->format('d M Y') }}
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" style="text-align:center;padding:2.5rem;color:#94a3b8;font-size:13px;">
|
||||
Belum ada data pengguna.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,253 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - Disnaker Kalbar</title>
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
:root{
|
||||
--primary:#2b3f8f;
|
||||
--primary-dark:#1f2f6b;
|
||||
--bg:#f4f6fb;
|
||||
--muted:#6b7280;
|
||||
--border:#e5e7eb;
|
||||
}
|
||||
|
||||
*{margin:0;padding:0;box-sizing:border-box;font-family:'Inter',sans-serif;}
|
||||
body{height:100vh;display:flex;}
|
||||
|
||||
.left{
|
||||
width:50%;
|
||||
background:
|
||||
linear-gradient(rgba(0,0,0,0.6), rgba(0,0,0,0.8)),
|
||||
url("{{ asset('bg-login.jpeg') }}");
|
||||
background-size:cover;
|
||||
background-position:center;
|
||||
color:white;
|
||||
padding:70px;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
justify-content:space-between;
|
||||
}
|
||||
|
||||
.brand{display:flex;align-items:center;gap:15px;}
|
||||
.brand img{height:100px;}
|
||||
|
||||
.brand-text{font-size:15px;font-weight:700;}
|
||||
|
||||
.hero{margin-top:-20px;max-width:520px;}
|
||||
|
||||
.hero h1{
|
||||
font-size:52px;
|
||||
font-weight:800;
|
||||
line-height:1.3;
|
||||
}
|
||||
|
||||
.hero h1::after{
|
||||
content:"";
|
||||
display:block;
|
||||
width:80px;
|
||||
height:4px;
|
||||
background:#60a5fa;
|
||||
margin-top:15px;
|
||||
border-radius:10px;
|
||||
}
|
||||
|
||||
.hero p{
|
||||
margin-top:20px;
|
||||
color:#dbeafe;
|
||||
line-height:1.8;
|
||||
}
|
||||
|
||||
.footer{
|
||||
font-size:12px;
|
||||
letter-spacing:2px;
|
||||
}
|
||||
|
||||
.right{
|
||||
width:50%;
|
||||
background:var(--bg);
|
||||
display:flex;
|
||||
justify-content:center;
|
||||
align-items:center;
|
||||
}
|
||||
|
||||
.card{
|
||||
width:100%;
|
||||
max-width:420px;
|
||||
}
|
||||
|
||||
.card h2{
|
||||
font-size:34px;
|
||||
margin-bottom:8px;
|
||||
}
|
||||
|
||||
.card p{
|
||||
color:var(--muted);
|
||||
margin-bottom:25px;
|
||||
}
|
||||
|
||||
.input-group{margin-bottom:18px;}
|
||||
|
||||
.input-group label{
|
||||
font-size:12px;
|
||||
color:var(--muted);
|
||||
font-weight:600;
|
||||
}
|
||||
|
||||
.input-box{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
border:1px solid var(--border);
|
||||
border-radius:12px;
|
||||
padding:14px;
|
||||
margin-top:6px;
|
||||
background:white;
|
||||
}
|
||||
|
||||
.input-box i{
|
||||
margin-right:10px;
|
||||
color:#9ca3af;
|
||||
}
|
||||
|
||||
.input-box input{
|
||||
border:none;
|
||||
outline:none;
|
||||
width:100%;
|
||||
font-size:14px;
|
||||
}
|
||||
|
||||
.btn{
|
||||
width:100%;
|
||||
padding:14px;
|
||||
background:var(--primary);
|
||||
color:white;
|
||||
border:none;
|
||||
border-radius:12px;
|
||||
font-weight:600;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
.btn:hover{background:var(--primary-dark);}
|
||||
|
||||
.error{
|
||||
background:#fee2e2;
|
||||
color:#991b1b;
|
||||
padding:10px;
|
||||
border-radius:8px;
|
||||
margin-bottom:15px;
|
||||
font-size:13px;
|
||||
}
|
||||
|
||||
.copy{
|
||||
text-align:center;
|
||||
font-size:12px;
|
||||
margin-top:25px;
|
||||
color:#9ca3af;
|
||||
}
|
||||
|
||||
.toggle-password{cursor:pointer;}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.left {
|
||||
width: 100%;
|
||||
padding: 40px 20px;
|
||||
min-height: 350px;
|
||||
gap: 30px;
|
||||
}
|
||||
.hero h1 {
|
||||
font-size: 36px;
|
||||
}
|
||||
.right {
|
||||
width: 100%;
|
||||
padding: 40px 20px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="left">
|
||||
<div class="brand">
|
||||
<img src="{{ asset('Kalimantan-Barat-Logo-Vector.png') }}">
|
||||
<div class="brand-text">
|
||||
DINAS TENAGA KERJA DAN TRANSMIGRASI<br>
|
||||
<span style="font-weight:400;font-size:12px;">PROVINSI KALIMANTAN BARAT</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero">
|
||||
<h1>Sistem Monitoring<br>Aktivitas Pegawai</h1>
|
||||
<p>
|
||||
Platform terintegrasi untuk pelaporan aktivitas, pemantauan kinerja pegawai,
|
||||
serta tata kelola administrasi yang transparan, akuntabel, dan profesional.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
TRANSPARANSI • AKUNTABILITAS • INTEGRITAS
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<div class="card">
|
||||
|
||||
<h2>Selamat Datang</h2>
|
||||
<p>Silakan masuk untuk mengakses panel monitoring pegawai.</p>
|
||||
|
||||
@if(session('error'))
|
||||
<div class="error">{{ session('error') }}</div>
|
||||
@endif
|
||||
|
||||
<!-- 🔥 FIX UTAMA DI SINI -->
|
||||
<form method="POST" action="{{ route('login.user') }}">
|
||||
@csrf
|
||||
|
||||
<div class="input-group">
|
||||
<label>USERNAME</label>
|
||||
<div class="input-box">
|
||||
<i class="fas fa-user"></i>
|
||||
<input type="text" name="username" value="{{ old('username') }}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>KATA SANDI</label>
|
||||
<div class="input-box">
|
||||
<i class="fas fa-lock"></i>
|
||||
<input type="password" name="password" id="password" required>
|
||||
<i class="fas fa-eye toggle-password" onclick="togglePassword()"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn">Masuk ke Sistem →</button>
|
||||
</form>
|
||||
|
||||
<div class="copy">
|
||||
© 2026 DINAS TENAGA KERJA DAN TRANSMIGRASI <br>
|
||||
PROVINSI KALIMANTAN BARAT
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function togglePassword() {
|
||||
const input = document.getElementById('password');
|
||||
input.type = input.type === 'password' ? 'text' : 'password';
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,334 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- STYLES --}}
|
||||
<style>
|
||||
/* Reset & Base */
|
||||
:root {
|
||||
--bg-main: #f4f7fb;
|
||||
--card-bg: #ffffff;
|
||||
--text-primary: #0f172a;
|
||||
--text-secondary: #475569;
|
||||
--text-muted: #94a3b8;
|
||||
--text-faint: #cbd5e1;
|
||||
--border-color: #e2e8f0;
|
||||
|
||||
--blue-dark: #0D215C;
|
||||
--blue-mid: #2563eb;
|
||||
--blue-pale: #bfdbfe;
|
||||
--blue-ultra: #eff6ff;
|
||||
|
||||
--green: #10b981;
|
||||
--red: #ef4444;
|
||||
--amber: #f59e0b;
|
||||
}
|
||||
|
||||
.sa-container {
|
||||
padding: 30px;
|
||||
background: var(--bg-main);
|
||||
min-height: calc(100vh - 70px);
|
||||
font-family: 'Outfit', 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
.sa-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.sa-title-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sa-title-wrap h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 900;
|
||||
color: #000;
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: -1px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.sa-title-wrap h1::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 50px;
|
||||
height: 4px;
|
||||
background: #000;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
color: var(--blue-mid);
|
||||
font-weight: 600;
|
||||
background: #fff;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-back:hover {
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Cards Grid */
|
||||
.news-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.news-card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.04);
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid rgba(226,232,240,0.6);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.news-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 12px 30px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.news-card a:hover .news-title {
|
||||
color: var(--blue-mid);
|
||||
}
|
||||
|
||||
.news-image {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
object-fit: cover;
|
||||
background-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.news-content {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.news-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.news-meta svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.news-title {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.4;
|
||||
margin: 0 0 12px 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.news-footer {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.status-badge.selesai { background: #ecfdf5; color: #059669; }
|
||||
.status-badge.proses { background: #eff6ff; color: #2563eb; }
|
||||
.status-badge.batal { background: #fef2f2; color: #dc2626; }
|
||||
|
||||
.btn-restore {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--blue-mid);
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-restore:hover {
|
||||
background: var(--blue-ultra);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
background: #fff;
|
||||
border-radius: 20px;
|
||||
border: 1px dashed var(--border-color);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sa-container {
|
||||
padding: 15px;
|
||||
}
|
||||
.sa-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
.btn-back {
|
||||
align-self: flex-start;
|
||||
}
|
||||
.news-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
{{-- ICONS --}}
|
||||
<svg width="0" height="0" style="position:absolute; display:none;">
|
||||
<symbol id="ic-calendar" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="16" y1="2" x2="16" y2="6"></line>
|
||||
<line x1="8" y1="2" x2="8" y2="6"></line>
|
||||
<line x1="3" y1="10" x2="21" y2="10"></line>
|
||||
</symbol>
|
||||
<symbol id="ic-eye" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
</symbol>
|
||||
<symbol id="ic-restore" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
|
||||
<path d="M3 3v5h5"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
<div class="sa-container">
|
||||
<div class="sa-header">
|
||||
<div class="sa-title-wrap">
|
||||
<h1>Arsip Laporan</h1>
|
||||
</div>
|
||||
<a href="{{ route('admin.superadmin.dashboard') }}" class="btn-back">← Kembali ke Dashboard</a>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div style="padding:14px 18px; background:#ecfdf5; border:1px solid #a7f3d0; border-radius:12px; color:#047857; font-size:14px; font-weight:600; margin-bottom:24px; display:flex; align-items:center; gap:10px;">
|
||||
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"></path></svg>
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div style="padding:14px 18px; background:#fef2f2; border:1px solid #fecaca; border-radius:12px; color:#dc2626; font-size:14px; font-weight:600; margin-bottom:24px; display:flex; align-items:center; gap:10px;">
|
||||
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path></svg>
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="news-grid">
|
||||
@forelse($activities as $act)
|
||||
@php
|
||||
// Generate a consistent dummy views count based on ID
|
||||
$views = ($act->id * 17) % 1000 + 50;
|
||||
|
||||
// Array of default placeholder images for variety
|
||||
$placeholders = [
|
||||
'https://images.unsplash.com/photo-1454165804606-c3d57bc86b40?q=80&w=600&auto=format&fit=crop', // office/planning
|
||||
'https://images.unsplash.com/photo-1517048676732-d65bc937f952?q=80&w=600&auto=format&fit=crop', // teamwork
|
||||
'https://images.unsplash.com/photo-1552664730-d307ca884978?q=80&w=600&auto=format&fit=crop', // meeting
|
||||
'https://images.unsplash.com/photo-1507537297725-24a1c029d3ca?q=80&w=600&auto=format&fit=crop', // paper/desk
|
||||
'https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?q=80&w=600&auto=format&fit=crop' // tech/research
|
||||
];
|
||||
$imgSrc = $placeholders[$act->id % count($placeholders)];
|
||||
@endphp
|
||||
<div class="news-card">
|
||||
<a href="{{ route('admin.superadmin.kegiatan.show', $act->id) }}" style="text-decoration: none; color: inherit; display: flex; flex-direction: column; flex-grow: 1;">
|
||||
<img src="{{ $imgSrc }}" alt="Laporan Image" class="news-image" loading="lazy">
|
||||
<div class="news-content" style="padding-bottom: 0; flex-grow: 1;">
|
||||
<div class="news-meta">
|
||||
<div class="meta-item">
|
||||
<svg><use href="#ic-calendar"/></svg>
|
||||
<span>{{ \Carbon\Carbon::parse($act->tanggal)->translatedFormat('d M Y') }}</span>
|
||||
</div>
|
||||
<span>•</span>
|
||||
<div class="meta-item">
|
||||
<svg><use href="#ic-eye"/></svg>
|
||||
<span>{{ $views }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="news-title" title="{{ $act->nama_kegiatan }}">{{ $act->nama_kegiatan }}</h3>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="news-content" style="padding-top: 10px; flex-grow: 0;">
|
||||
<div class="news-footer">
|
||||
<span class="status-badge {{ $act->status == 'selesai' ? 'selesai' : ($act->status == 'dibatalkan' ? 'batal' : 'proses') }}">
|
||||
{{ strtoupper(str_replace('_', ' ', $act->status)) }}
|
||||
</span>
|
||||
|
||||
<form action="{{ route('admin.superadmin.kegiatan.archive', $act->id) }}" method="POST" style="margin: 0;">
|
||||
@csrf
|
||||
<button type="submit" class="btn-restore" title="Kembalikan Laporan">
|
||||
<svg width="16" height="16"><use href="#ic-restore"/></svg>
|
||||
Restore
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="empty-state">
|
||||
<svg width="64" height="64" style="color:var(--text-faint); margin-bottom:15px;" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 002-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>
|
||||
<h3>Belum ada arsip laporan</h3>
|
||||
<p>Laporan yang diarsipkan akan muncul di sini.</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
@if($activities->hasPages())
|
||||
<div style="margin-top:40px; display:flex; justify-content:center;">
|
||||
{{ $activities->links('pagination::bootstrap-4') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,125 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('content')
|
||||
|
||||
<style>
|
||||
.page-container { padding: 40px 20px; background: #f4f7fb; min-height: calc(100vh - 70px); font-family: 'Outfit', 'Segoe UI', sans-serif; display: flex; flex-direction: column; align-items: center; }
|
||||
.form-card { background: #fff; border-radius: 24px; box-shadow: 0 10px 40px rgba(0,0,0,0.03); width: 100%; max-width: 600px; padding: 35px 40px; box-sizing: border-box; }
|
||||
.date-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 35px; }
|
||||
.page-title { font-size: 28px; font-weight: 800; color: #0f172a; margin: 0 0 8px; letter-spacing: -0.5px; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-container { padding: 20px 15px !important; }
|
||||
.form-card { padding: 25px 20px !important; border-radius: 16px !important; }
|
||||
.date-grid { grid-template-columns: 1fr !important; gap: 15px !important; margin-bottom: 25px !important; }
|
||||
.page-title { font-size: 24px !important; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="page-container">
|
||||
|
||||
{{-- HEADER --}}
|
||||
<div style="text-align:center; margin-bottom:30px;">
|
||||
<h1 class="page-title">Buat Penugasan Baru</h1>
|
||||
<p style="margin:0; color:#64748b; font-size:15px; font-weight:500;">Berikan instruksi tugas kepada pegawai lapangan</p>
|
||||
</div>
|
||||
|
||||
@if(session('error'))
|
||||
<div style="width:100%; max-width:600px; margin-bottom:20px; padding:14px 18px; background:#fff1f2; border:1px solid #fecdd3; border-radius:12px; color:#be123c; font-size:14px; font-weight:600; display:flex; align-items:center; gap:8px;">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- FORM CARD --}}
|
||||
<div class="form-card">
|
||||
<form action="{{ route('admin.superadmin.kegiatan.store') }}" method="POST">
|
||||
@csrf
|
||||
|
||||
@if ($errors->any())
|
||||
<div style="margin-bottom:20px; padding:14px 18px; background:#fff1f2; border:1px solid #fecdd3; border-radius:12px;">
|
||||
<p style="font-weight:700; color:#be123c; margin:0 0 8px; font-size:13px;">Terdapat kesalahan input:</p>
|
||||
<ul style="margin:0; padding-left:18px; color:#be123c; font-size:13px;">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div style="margin-bottom:24px;">
|
||||
<label style="display:block; font-size:11px; font-weight:800; color:#94a3b8; letter-spacing:1.5px; margin-bottom:8px; text-transform:uppercase;">
|
||||
Nomor Surat
|
||||
</label>
|
||||
<input type="text" name="nomor_surat" value="{{ old('nomor_surat') }}"
|
||||
style="width:100%; padding:14px 16px; background:#f8fafc; border:2px solid transparent; border-radius:12px; font-size:15px; color:#0f172a; outline:none; box-sizing:border-box; transition:all 0.2s; font-family:inherit; font-weight:500;"
|
||||
onfocus="this.style.borderColor='#e0e7ff'; this.style.background='#fff'" onblur="this.style.borderColor='transparent'; this.style.background='#f8fafc'"
|
||||
placeholder="Contoh: 001/ST/V/2026" required>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:24px;">
|
||||
<label style="display:block; font-size:11px; font-weight:800; color:#94a3b8; letter-spacing:1.5px; margin-bottom:8px; text-transform:uppercase;">
|
||||
Nama Kegiatan
|
||||
</label>
|
||||
<input type="text" name="kegiatan" value="{{ old('kegiatan') }}"
|
||||
style="width:100%; padding:14px 16px; background:#f8fafc; border:2px solid transparent; border-radius:12px; font-size:15px; color:#0f172a; outline:none; box-sizing:border-box; transition:all 0.2s; font-family:inherit; font-weight:500;"
|
||||
onfocus="this.style.borderColor='#e0e7ff'; this.style.background='#fff'" onblur="this.style.borderColor='transparent'; this.style.background='#f8fafc'"
|
||||
placeholder="Contoh: Monitoring Jaringan Dishub" required>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:24px;">
|
||||
<label style="display:block; font-size:11px; font-weight:800; color:#94a3b8; letter-spacing:1.5px; margin-bottom:8px; text-transform:uppercase;">
|
||||
Pilih Petugas (Multiple)
|
||||
</label>
|
||||
<select name="pegawai_ids[]" multiple required
|
||||
style="width:100%; padding:14px 16px; background:#f8fafc; border:2px solid transparent; border-radius:12px; font-size:14px; color:#334155; outline:none; box-sizing:border-box; transition:all 0.2s; font-family:inherit; font-weight:500; height:140px; line-height:1.5;"
|
||||
onfocus="this.style.borderColor='#e0e7ff'; this.style.background='#fff'" onblur="this.style.borderColor='transparent'; this.style.background='#f8fafc'">
|
||||
@foreach($users as $user)
|
||||
<option value="{{ $user['id'] }}" style="padding:6px 0; border-bottom:1px solid #f1f5f9;">
|
||||
{{ $user['name'] }} - {{ $user['jabatan'] ?? 'Pegawai' }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<p style="font-size:11px; color:#94a3b8; margin-top:8px; font-weight:500;">* Tahan Ctrl/Cmd untuk memilih lebih dari 1 orang</p>
|
||||
</div>
|
||||
|
||||
<div class="date-grid">
|
||||
<div>
|
||||
<label style="display:block; font-size:11px; font-weight:800; color:#94a3b8; letter-spacing:1.5px; margin-bottom:8px; text-transform:uppercase;">
|
||||
Tanggal Mulai
|
||||
</label>
|
||||
<input type="date" name="tanggal_mulai" value="{{ old('tanggal_mulai') }}"
|
||||
style="width:100%; padding:14px 16px; background:#f8fafc; border:2px solid transparent; border-radius:12px; font-size:14px; color:#334155; outline:none; box-sizing:border-box; transition:all 0.2s; font-family:inherit; font-weight:500; color-scheme:light;"
|
||||
onfocus="this.style.borderColor='#e0e7ff'; this.style.background='#fff'" onblur="this.style.borderColor='transparent'; this.style.background='#f8fafc'" required>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block; font-size:11px; font-weight:800; color:#94a3b8; letter-spacing:1.5px; margin-bottom:8px; text-transform:uppercase;">
|
||||
Tanggal Selesai
|
||||
</label>
|
||||
<input type="date" name="tanggal_selesai" value="{{ old('tanggal_selesai') }}"
|
||||
style="width:100%; padding:14px 16px; background:#f8fafc; border:2px solid transparent; border-radius:12px; font-size:14px; color:#334155; outline:none; box-sizing:border-box; transition:all 0.2s; font-family:inherit; font-weight:500; color-scheme:light;"
|
||||
onfocus="this.style.borderColor='#e0e7ff'; this.style.background='#fff'" onblur="this.style.borderColor='transparent'; this.style.background='#f8fafc'">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
style="width:100%; padding:16px; background:#0D215C; color:#fff; border:none; border-radius:12px; font-size:15px; font-weight:800; cursor:pointer; font-family:inherit; transition:all 0.2s;"
|
||||
onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 8px 20px rgba(13,33,92,0.2)'"
|
||||
onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
|
||||
Konfirmasi & Kirim Tugas
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- KEMBALI LINK --}}
|
||||
<div style="margin-top:30px;">
|
||||
<a href="{{ route('admin.superadmin.dashboard') }}"
|
||||
style="font-size:13px; font-weight:700; color:#64748b; letter-spacing:1px; text-decoration:none; text-transform:uppercase; transition:color 0.2s;"
|
||||
onmouseover="this.style.color='#0D215C'" onmouseout="this.style.color='#64748b'">
|
||||
Kembali Ke Dashboard
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,202 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
.page-container { padding: 40px 20px; background: #f4f7fb; min-height: calc(100vh - 70px); font-family: 'Outfit', 'Segoe UI', sans-serif; display: flex; flex-direction: column; align-items: center; box-sizing: border-box; }
|
||||
.detail-card { background: #fff; border-radius: 24px; box-shadow: 0 10px 40px rgba(0,0,0,0.03); width: 100%; max-width: 700px; overflow: hidden; box-sizing: border-box; }
|
||||
.card-header { padding: 30px 40px; border-bottom: 1px solid #f1f5f9; display: flex; justify-content: space-between; align-items: flex-start; }
|
||||
.info-grid { padding: 30px 40px; display: flex; gap: 30px; border-top: 1px solid #f1f5f9; }
|
||||
.section-padding { padding: 30px 40px; }
|
||||
.action-card { margin-top: 20px; background: white; padding: 24px 30px; border-radius: 16px; border: 1px solid #e2e8f0; box-shadow: 0 4px 6px rgba(0,0,0,0.02); width: 100%; max-width: 700px; box-sizing: border-box; }
|
||||
.action-flex { display: flex; gap: 16px; flex-wrap: wrap; }
|
||||
.revision-form { display: flex; gap: 10px; flex: 1; min-width: 300px; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-container { padding: 20px 15px !important; }
|
||||
.detail-card { border-radius: 16px !important; }
|
||||
.card-header { flex-direction: column !important; gap: 20px !important; padding: 20px 20px !important; }
|
||||
.card-header > div:last-child { text-align: left !important; }
|
||||
.info-grid { flex-direction: column !important; gap: 20px !important; padding: 20px 20px !important; }
|
||||
.section-padding { padding: 20px 20px !important; }
|
||||
.action-card { padding: 20px !important; }
|
||||
.revision-form { flex-direction: column !important; min-width: 100% !important; }
|
||||
.revision-form button { width: 100% !important; }
|
||||
}
|
||||
</style>
|
||||
<div class="page-container">
|
||||
|
||||
{{-- HEADER --}}
|
||||
<div style="text-align:center; margin-bottom:30px;">
|
||||
<h1 style="font-size:28px; font-weight:800; color:#0f172a; margin:0 0 8px; letter-spacing:-0.5px;">Detail Penugasan</h1>
|
||||
<p style="margin:0; color:#64748b; font-size:15px; font-weight:500;">Tinjauan lengkap tentang kegiatan dan petugas yang ditugaskan</p>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div style="width:100%; max-width:700px; margin-bottom:20px; padding:14px 18px; background:#ecfdf5; border:1px solid #a7f3d0; border-radius:12px; color:#047857; font-size:14px; font-weight:600; display:flex; align-items:center; gap:8px;">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg>
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- DETAIL CARD --}}
|
||||
<div class="detail-card">
|
||||
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span style="display:inline-block; padding:6px 12px; border-radius:20px; font-size:11px; font-weight:800; letter-spacing:1px; text-transform:uppercase; margin-bottom:12px;
|
||||
{{ $activity->status == 'selesai' ? 'background:#ecfdf5; color:#059669;' :
|
||||
($activity->status == 'dibatalkan' ? 'background:#fef2f2; color:#dc2626;' : 'background:#eff6ff; color:#2563eb;') }}">
|
||||
{{ str_replace('_', ' ', $activity->status) }}
|
||||
</span>
|
||||
<h2 style="margin:0 0 5px; font-size:22px; font-weight:800; color:#0f172a;">{{ $activity->nama_kegiatan }}</h2>
|
||||
<div style="color:#64748b; font-size:13px; font-weight:500;">Nomor Surat: {{ $activity->nomor_surat ?? '-' }}</div>
|
||||
</div>
|
||||
<div style="text-align:right;">
|
||||
<div style="font-size:11px; font-weight:800; color:#94a3b8; letter-spacing:1px; text-transform:uppercase; margin-bottom:4px;">Tanggal Mulai</div>
|
||||
<div style="font-size:15px; font-weight:700; color:#0f172a; margin-bottom:10px;">{{ \Carbon\Carbon::parse($activity->tanggal)->translatedFormat('d F Y') }}</div>
|
||||
|
||||
<div style="font-size:11px; font-weight:800; color:#94a3b8; letter-spacing:1px; text-transform:uppercase; margin-bottom:4px;">Tenggat Waktu</div>
|
||||
<div style="font-size:15px; font-weight:700; color:#0f172a;">
|
||||
@if($activity->tanggal_selesai)
|
||||
@php
|
||||
$isOverdue = \Carbon\Carbon::parse($activity->tanggal_selesai)->isPast() && $activity->status !== 'selesai' && $activity->status !== 'telah_ditinjau';
|
||||
@endphp
|
||||
<span style="{{ $isOverdue ? 'color:#dc2626; font-weight:800;' : '' }}">
|
||||
{{ \Carbon\Carbon::parse($activity->tanggal_selesai)->translatedFormat('d F Y') }}
|
||||
@if($isOverdue) <span style="font-size:11px; color:#ef4444; margin-left:4px;">(Terlambat)</span> @endif
|
||||
</span>
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-padding" style="background:#f8fafc;">
|
||||
<h3 style="font-size:13px; font-weight:800; color:#64748b; letter-spacing:1.5px; text-transform:uppercase; margin:0 0 16px;">Ditugaskan Kepada</h3>
|
||||
|
||||
<div style="display:flex; flex-direction:column; gap:12px;">
|
||||
@forelse($activity->petugas ?? [] as $petugas)
|
||||
<div style="display:flex; align-items:center; gap:15px; background:#fff; padding:12px 16px; border-radius:12px; border:1px solid #e2e8f0;">
|
||||
<div style="width:40px; height:40px; border-radius:50%; background:#e0e7ff; color:#3730a3; display:flex; align-items:center; justify-content:center; font-weight:800; font-size:14px;">
|
||||
{{ strtoupper(substr($petugas['name'], 0, 2)) }}
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:14px; font-weight:700; color:#0f172a; margin-bottom:2px;">{{ $petugas['name'] }}</div>
|
||||
<div style="font-size:12px; color:#64748b; font-weight:500;">{{ $petugas['jabatan'] ?? 'Petugas Lapangan' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div style="font-size:13px; color:#94a3b8; font-style:italic;">Belum ada petugas yang ditetapkan.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div style="flex:1;">
|
||||
<div style="font-size:11px; font-weight:800; color:#94a3b8; letter-spacing:1px; text-transform:uppercase; margin-bottom:6px;">Lokasi Tugas</div>
|
||||
<div style="font-size:14px; font-weight:600; color:#0f172a;">{{ $activity->lokasi ?? '-' }}</div>
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<div style="font-size:11px; font-weight:800; color:#94a3b8; letter-spacing:1px; text-transform:uppercase; margin-bottom:6px;">Deskripsi Tugas</div>
|
||||
<div style="font-size:14px; font-weight:500; color:#334155;">{{ $activity->deskripsi ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(!empty($activity->status_note))
|
||||
<div class="section-padding" style="background:#eff6ff; border-top:1px solid #bfdbfe;">
|
||||
<div style="font-size:11px; font-weight:800; color:#2563eb; letter-spacing:1px; text-transform:uppercase; margin-bottom:8px;">Catatan Status Terakhir</div>
|
||||
<div style="font-size:14px; font-weight:500; color:#1e3a8a; line-height:1.6;">{{ $activity->status_note }}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- TIMELINE PROGRESS MONITORING --}}
|
||||
<div class="section-padding" style="background:#ffffff; border-top:1px solid #f1f5f9;">
|
||||
<div style="font-size:11px; font-weight:800; color:#94a3b8; letter-spacing:1px; text-transform:uppercase; margin-bottom:20px;">Log Monitoring Progress</div>
|
||||
|
||||
@if(isset($activity->progress) && count($activity->progress) > 0)
|
||||
<div style="position:relative; border-left:2px solid #e0e7ff; margin-left:10px; padding-left:20px; display:flex; flex-direction:column; gap:24px;">
|
||||
@foreach($activity->progress as $prog)
|
||||
<div style="position:relative;">
|
||||
<div style="position:absolute; left:-27px; top:4px; width:12px; height:12px; background:#6366f1; border-radius:50%; border:2px solid #fff;"></div>
|
||||
<div style="font-size:12px; color:#64748b; margin-bottom:4px;">
|
||||
{{ \Carbon\Carbon::parse($prog['created_at'])->format('d M Y, H:i') }} •
|
||||
<span style="font-weight:700; color:#6366f1;">{{ $prog['user']['name'] ?? 'Pegawai' }}</span>
|
||||
</div>
|
||||
<div style="font-size:14px; color:#334155; background:#f8fafc; border:1px solid #f1f5f9; padding:12px 16px; border-radius:10px;">
|
||||
{{ $prog['note'] }}
|
||||
@if(!empty($prog['attachment']))
|
||||
<div style="margin-top:10px; padding-top:10px; border-top:1px solid #e2e8f0;">
|
||||
<a href="http://127.0.0.1:8000/{{ $prog['attachment'] }}" target="_blank"
|
||||
style="display:inline-block; font-size:12px; font-weight:600; color:#4f46e5; text-decoration:none; background:#e0e7ff; padding:4px 10px; border-radius:6px; border:1px solid #c7d2fe;">
|
||||
📎 Lihat Lampiran Bukti
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div style="font-size:13px; color:#64748b; background:#f8fafc; padding:20px; border-radius:10px; text-align:center; border:1px dashed #cbd5e1;">
|
||||
Belum ada log progress yang dicatat untuk kegiatan ini.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div style="flex:1;">
|
||||
<div style="font-size:11px; font-weight:800; color:#94a3b8; letter-spacing:1px; text-transform:uppercase; margin-bottom:6px;">Dibuat Oleh</div>
|
||||
<div style="font-size:14px; font-weight:600; color:#0f172a;">{{ $activity->user->name ?? 'System' }}</div>
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<div style="font-size:11px; font-weight:800; color:#94a3b8; letter-spacing:1px; text-transform:uppercase; margin-bottom:6px;">Pembaruan Terakhir</div>
|
||||
<div style="font-size:14px; font-weight:600; color:#0f172a;">{{ $activity->updated_at->diffForHumans() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- TINDAKAN REVIEW (SUPERADMIN) --}}
|
||||
@if($activity->status == 'selesai')
|
||||
<div class="action-card">
|
||||
<h4 style="margin-top:0; margin-bottom:16px; font-size:16px; color:#1e293b;">Tindakan Review (Verifikasi)</h4>
|
||||
|
||||
<div class="action-flex">
|
||||
{{-- Setujui (Telah Ditinjau) --}}
|
||||
<form action="{{ route('admin.superadmin.kegiatan.approve', $activity->id) }}" method="POST">
|
||||
@csrf
|
||||
<button type="submit" onclick="return confirm('Setujui laporan ini?')"
|
||||
style="background:#16a34a; color:white; border:none; padding:10px 20px; border-radius:10px; font-weight:600; font-size:14px; cursor:pointer; transition:0.2s;"
|
||||
onmouseover="this.style.background='#15803d'" onmouseout="this.style.background='#16a34a'">
|
||||
✓ Setujui (Telah Ditinjau)
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{{-- Minta Revisi --}}
|
||||
<form action="{{ route('admin.superadmin.kegiatan.revision', $activity->id) }}" method="POST" class="revision-form">
|
||||
@csrf
|
||||
<input type="text" name="status_note" required placeholder="Catatan Revisi (misal: Mohon perbaiki foto)"
|
||||
style="flex:1; padding:10px 16px; border:1px solid #cbd5e1; border-radius:10px; font-size:14px; outline:none;"
|
||||
onfocus="this.style.borderColor='#3b82f6'" onblur="this.style.borderColor='#cbd5e1'">
|
||||
<button type="submit" onclick="return confirm('Kembalikan laporan ini ke pegawai untuk direvisi?')"
|
||||
style="background:#f59e0b; color:white; border:none; padding:10px 20px; border-radius:10px; font-weight:600; font-size:14px; cursor:pointer; transition:0.2s; white-space:nowrap;"
|
||||
onmouseover="this.style.background='#d97706'" onmouseout="this.style.background='#f59e0b'">
|
||||
⟲ Minta Revisi
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- KEMBALI LINK --}}
|
||||
<div style="margin-top:30px;">
|
||||
<a href="{{ route('admin.superadmin.dashboard') }}"
|
||||
style="font-size:13px; font-weight:700; color:#64748b; letter-spacing:1px; text-decoration:none; text-transform:uppercase; transition:color 0.2s;"
|
||||
onmouseover="this.style.color='#0D215C'" onmouseout="this.style.color='#64748b'">
|
||||
Kembali Ke Dashboard
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,222 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('content')
|
||||
|
||||
<style>
|
||||
.page-container { padding: 28px 32px; background: #f1f5f9; min-height: 100vh; font-family: 'Figtree', 'Segoe UI', sans-serif; }
|
||||
.header-flex { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 28px; }
|
||||
.main-grid { display: grid; grid-template-columns: 1fr 300px; gap: 20px; align-items: flex-start; }
|
||||
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; }
|
||||
.form-actions { border-top: 1px solid #f1f5f9; padding-top: 20px; display: flex; gap: 10px; justify-content: flex-end; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-container { padding: 16px 12px !important; }
|
||||
.header-flex { flex-direction: column !important; gap: 16px !important; }
|
||||
.main-grid { grid-template-columns: 1fr !important; }
|
||||
.form-grid { grid-template-columns: 1fr !important; gap: 15px !important; }
|
||||
.form-actions { flex-direction: column !important; }
|
||||
.form-actions a, .form-actions button { width: 100% !important; justify-content: center; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="page-container">
|
||||
|
||||
{{-- HEADER --}}
|
||||
<div class="header-flex">
|
||||
<div>
|
||||
<a href="{{ route('admin.superadmin.user.index') }}"
|
||||
style="font-size:12px; color:#64748b; text-decoration:none; display:inline-flex; align-items:center; gap:5px; margin-bottom:10px; font-weight:600;"
|
||||
onmouseover="this.style.color='#0D215C'" onmouseout="this.style.color='#64748b'">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
||||
<path d="M19 12H5M12 5l-7 7 7 7"/>
|
||||
</svg>
|
||||
Kembali ke Manajemen Pengguna
|
||||
</a>
|
||||
<p style="font-size:11px; font-weight:700; color:#94a3b8; letter-spacing:1.8px; margin:0 0 6px; text-transform:uppercase;">Panel Kendali Sistem</p>
|
||||
<h1 style="font-size:28px; font-weight:800; color:#0f172a; margin:0 0 4px;">Tambah Pengguna</h1>
|
||||
<p style="margin:0; color:#94a3b8; font-size:13px;">Buat akun baru untuk pegawai atau administrator sistem</p>
|
||||
</div>
|
||||
<div style="background:#eef2ff; color:#4338ca; padding:9px 14px; border-radius:10px; font-size:12px; font-weight:700; display:flex; align-items:center; gap:6px;">
|
||||
<div style="width:8px; height:8px; background:#4338ca; border-radius:50%;"></div>
|
||||
{{ strtoupper(session('user')['role'] ?? 'ADMINISTRATOR') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(session('error'))
|
||||
<div style="margin-bottom:20px; padding:14px 18px; background:#fff1f2; border:1px solid #fecdd3; border-radius:12px; color:#be123c; font-size:14px; font-weight:600; display:flex; align-items:center; gap:8px;">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="main-grid">
|
||||
|
||||
{{-- FORM CARD --}}
|
||||
<div style="background:#fff; border-radius:16px; border:1px solid #e2e8f0; overflow:hidden;">
|
||||
<div style="padding:20px 24px; border-bottom:1px solid #f1f5f9; display:flex; align-items:center; gap:10px;">
|
||||
<div style="width:32px; height:32px; background:#eff6ff; border-radius:8px; display:flex; align-items:center; justify-content:center;">
|
||||
<svg width="16" height="16" fill="none" stroke="#2563eb" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<line x1="19" y1="8" x2="19" y2="14"/>
|
||||
<line x1="22" y1="11" x2="16" y2="11"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 style="margin:0; font-size:16px; font-weight:700; color:#0f172a;">Data Pengguna Baru</h3>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('admin.superadmin.user.store') }}" method="POST" style="padding:28px 24px;">
|
||||
@csrf
|
||||
|
||||
@if ($errors->any())
|
||||
<div style="margin-bottom:20px; padding:14px 18px; background:#fff1f2; border:1px solid #fecdd3; border-radius:12px;">
|
||||
<p style="font-weight:700; color:#be123c; margin:0 0 8px; font-size:13px;">Terdapat kesalahan input:</p>
|
||||
<ul style="margin:0; padding-left:18px; color:#be123c; font-size:13px;">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="form-grid">
|
||||
{{-- NAMA --}}
|
||||
<div>
|
||||
<label style="display:block; font-size:13px; font-weight:700; color:#374151; margin-bottom:8px;">
|
||||
Nama Lengkap <span style="color:#ef4444;">*</span>
|
||||
</label>
|
||||
<input type="text" name="name" value="{{ old('name') }}"
|
||||
style="width:100%; padding:11px 14px; border:1.5px solid #e2e8f0; border-radius:10px; font-size:14px; color:#0f172a; outline:none; box-sizing:border-box;"
|
||||
onfocus="this.style.borderColor='#2563eb'" onblur="this.style.borderColor='#e2e8f0'"
|
||||
placeholder="Nama lengkap pegawai" required>
|
||||
</div>
|
||||
|
||||
{{-- USERNAME --}}
|
||||
<div>
|
||||
<label style="display:block; font-size:13px; font-weight:700; color:#374151; margin-bottom:8px;">
|
||||
Username <span style="color:#ef4444;">*</span>
|
||||
</label>
|
||||
<input type="text" name="username" value="{{ old('username') }}"
|
||||
style="width:100%; padding:11px 14px; border:1.5px solid #e2e8f0; border-radius:10px; font-size:14px; color:#0f172a; outline:none; box-sizing:border-box;"
|
||||
onfocus="this.style.borderColor='#2563eb'" onblur="this.style.borderColor='#e2e8f0'"
|
||||
placeholder="Username untuk login" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-grid">
|
||||
{{-- PASSWORD --}}
|
||||
<div>
|
||||
<label style="display:block; font-size:13px; font-weight:700; color:#374151; margin-bottom:8px;">
|
||||
Kata Sandi <span style="color:#ef4444;">*</span>
|
||||
</label>
|
||||
<div style="position:relative;">
|
||||
<input type="password" name="password" id="password_field"
|
||||
style="width:100%; padding:11px 40px 11px 14px; border:1.5px solid #e2e8f0; border-radius:10px; font-size:14px; color:#0f172a; outline:none; box-sizing:border-box;"
|
||||
onfocus="this.style.borderColor='#2563eb'" onblur="this.style.borderColor='#e2e8f0'"
|
||||
placeholder="Minimal 6 karakter" required>
|
||||
<button type="button" onclick="togglePassword('password_field', this)"
|
||||
style="position:absolute; right:12px; top:50%; transform:translateY(-50%); background:none; border:none; cursor:pointer; color:#94a3b8; padding:0; display:flex; align-items:center; justify-content:center;">
|
||||
<svg class="eye-icon" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
<svg class="eye-off-icon" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="display:none;">
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/>
|
||||
<line x1="1" y1="1" x2="23" y2="23"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ROLE --}}
|
||||
<div>
|
||||
<label style="display:block; font-size:13px; font-weight:700; color:#374151; margin-bottom:8px;">
|
||||
Hak Akses (Role) <span style="color:#ef4444;">*</span>
|
||||
</label>
|
||||
<select name="role"
|
||||
style="width:100%; padding:11px 14px; border:1.5px solid #e2e8f0; border-radius:10px; font-size:14px; color:#0f172a; outline:none; box-sizing:border-box; background:#fff;"
|
||||
onfocus="this.style.borderColor='#2563eb'" onblur="this.style.borderColor='#e2e8f0'">
|
||||
<option value="user" {{ old('role') == 'user' ? 'selected' : '' }}>Petugas (User)</option>
|
||||
<option value="admin" {{ old('role') == 'admin' ? 'selected' : '' }}>Admin (KASI)</option>
|
||||
<option value="superadmin" {{ old('role') == 'superadmin' ? 'selected' : '' }}>Super Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<a href="{{ route('admin.superadmin.user.index') }}"
|
||||
style="padding:10px 20px; border:1.5px solid #e2e8f0; border-radius:10px; font-size:13px; font-weight:700; color:#64748b; text-decoration:none; background:#f8fafc;"
|
||||
onmouseover="this.style.background='#e2e8f0'" onmouseout="this.style.background='#f8fafc'">
|
||||
Batal
|
||||
</a>
|
||||
<button type="submit"
|
||||
style="padding:10px 24px; background:#0D215C; color:#fff; border:none; border-radius:10px; font-size:13px; font-weight:700; cursor:pointer; display:flex; align-items:center; gap:7px;"
|
||||
onmouseover="this.style.opacity='.85'" onmouseout="this.style.opacity='1'">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<line x1="19" y1="8" x2="19" y2="14"/>
|
||||
<line x1="22" y1="11" x2="16" y2="11"/>
|
||||
</svg>
|
||||
Tambah Pengguna
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- TIPS CARD --}}
|
||||
<div>
|
||||
<div style="background:#fff; border-radius:16px; border:1px solid #e2e8f0; padding:20px; margin-bottom:16px;">
|
||||
<div style="display:flex; align-items:center; gap:8px; margin-bottom:12px;">
|
||||
<div style="width:28px; height:28px; background:#f0fdf4; border-radius:8px; display:flex; align-items:center; justify-content:center;">
|
||||
<svg width="14" height="14" fill="none" stroke="#10b981" stroke-width="2" viewBox="0 0 24 24">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span style="font-size:14px; font-weight:700; color:#0f172a;">Panduan Pengisian</span>
|
||||
</div>
|
||||
<ul style="margin:0; padding-left:16px; font-size:12px; color:#64748b; line-height:1.9;">
|
||||
<li>Nama sesuai dengan dokumen resmi</li>
|
||||
<li>Username digunakan untuk login</li>
|
||||
|
||||
<li>Kata sandi minimal 6 karakter</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="background:#eff6ff; border-radius:16px; border:1px solid #bfdbfe; padding:18px 20px;">
|
||||
<div style="display:flex; align-items:center; gap:8px; margin-bottom:10px;">
|
||||
<svg width="16" height="16" fill="#2563eb" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
||||
<span style="font-size:13px; font-weight:700; color:#1d4ed8;">Panduan Role</span>
|
||||
</div>
|
||||
<ul style="margin:0; padding-left:16px; font-size:12px; color:#3b82f6; line-height:1.8;">
|
||||
<li><strong>Petugas</strong>: Input kegiatan harian</li>
|
||||
<li><strong>Admin</strong>: Monitor kegiatan & user</li>
|
||||
<li><strong>Super Admin</strong>: Akses penuh sistem</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
<script>
|
||||
function togglePassword(inputId, buttonElement) {
|
||||
const input = document.getElementById(inputId);
|
||||
const eyeIcon = buttonElement.querySelector('.eye-icon');
|
||||
const eyeOffIcon = buttonElement.querySelector('.eye-off-icon');
|
||||
|
||||
if (input.type === 'password') {
|
||||
input.type = 'text';
|
||||
eyeIcon.style.display = 'none';
|
||||
eyeOffIcon.style.display = 'block';
|
||||
buttonElement.style.color = '#2563eb';
|
||||
} else {
|
||||
input.type = 'password';
|
||||
eyeIcon.style.display = 'block';
|
||||
eyeOffIcon.style.display = 'none';
|
||||
buttonElement.style.color = '#94a3b8';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,346 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('content')
|
||||
|
||||
<style>
|
||||
.page-container { padding: 28px 32px; background: #f1f5f9; min-height: 100vh; font-family: 'Figtree', 'Segoe UI', sans-serif; }
|
||||
.header-flex { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 28px; }
|
||||
.main-grid { display: grid; grid-template-columns: 1fr 300px; gap: 24px; align-items: flex-start; }
|
||||
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
||||
.form-actions { border-top: 1px solid #f1f5f9; padding-top: 24px; display: flex; justify-content: space-between; align-items: center; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-container { padding: 16px 12px !important; }
|
||||
.header-flex { flex-direction: column !important; gap: 16px !important; align-items: flex-start !important; }
|
||||
.main-grid { grid-template-columns: 1fr !important; }
|
||||
.form-grid { grid-template-columns: 1fr !important; gap: 15px !important; }
|
||||
.form-actions { flex-direction: column !important; gap: 16px !important; align-items: flex-start !important; }
|
||||
.form-actions a, .form-actions button { width: 100% !important; justify-content: center; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="page-container">
|
||||
|
||||
{{-- HEADER --}}
|
||||
<div class="header-flex">
|
||||
<div>
|
||||
<div style="display:flex; align-items:center; gap:8px; margin-bottom:8px;">
|
||||
<div style="width:24px; height:2px; background:#3b82f6;"></div>
|
||||
<p style="font-size:11px; font-weight:700; color:#3b82f6; letter-spacing:1px; margin:0; text-transform:uppercase;">
|
||||
Panel Kendali Sistem
|
||||
</p>
|
||||
</div>
|
||||
<h1 style="font-size:28px; font-weight:800; color:#0f172a; margin:0 0 4px;">
|
||||
Edit Pengguna
|
||||
</h1>
|
||||
<p style="margin:0; color:#64748b; font-size:13px;">
|
||||
Ubah informasi akun dan hak akses pengguna
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; align-items:center; gap:12px;">
|
||||
<a href="{{ route('admin.superadmin.user.index') }}"
|
||||
style="background:#fff; border:1px solid #e2e8f0; padding:8px 16px; border-radius:999px; font-size:12px; font-weight:600; color:#475569; text-decoration:none; display:flex; align-items:center; gap:6px;">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path d="M19 12H5M12 5l-7 7 7 7"/>
|
||||
</svg>
|
||||
Kembali ke Daftar
|
||||
</a>
|
||||
<div style="background:#fef3c7; color:#b45309; border:1px solid #fde68a; padding:8px 16px; border-radius:999px; font-size:11px; font-weight:800; display:flex; align-items:center; gap:6px; letter-spacing:0.5px;">
|
||||
<svg width="12" height="12" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||||
</svg>
|
||||
SUPER ADMIN
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div style="margin-bottom:20px; padding:14px 18px; background:#f0fdf4; border:1px solid #bbf7d0; border-radius:12px; color:#15803d; font-size:14px; font-weight:600; display:flex; align-items:center; gap:8px;">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div style="margin-bottom:20px; padding:14px 18px; background:#fff1f2; border:1px solid #fecdd3; border-radius:12px; color:#be123c; font-size:14px; font-weight:600; display:flex; align-items:center; gap:8px;">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
@if ($errors->any())
|
||||
<div style="margin-bottom:20px; padding:14px 18px; background:#fff1f2; border:1px solid #fecdd3; border-radius:12px;">
|
||||
<p style="font-weight:700; color:#be123c; margin:0 0 8px; font-size:13px;">Terdapat kesalahan input:</p>
|
||||
<ul style="margin:0; padding-left:18px; color:#be123c; font-size:13px;">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="main-grid">
|
||||
|
||||
{{-- LEFT SIDEBAR --}}
|
||||
<div>
|
||||
{{-- Profile Card --}}
|
||||
<div style="background:#fff; border-radius:16px; border:1px solid #e2e8f0; overflow:hidden; margin-bottom:16px; box-shadow:0 4px 6px -1px rgba(0,0,0,0.05);">
|
||||
{{-- Gradient Top --}}
|
||||
<div style="background: linear-gradient(135deg, #2563eb 0%, #6366f1 100%); padding:32px 20px; text-align:center; position:relative;">
|
||||
|
||||
@php
|
||||
$initials = strtoupper(substr($user->name, 0, 2));
|
||||
@endphp
|
||||
<div style="width:72px; height:72px; background:rgba(255,255,255,0.2); border:2px solid rgba(255,255,255,0.4); border-radius:20px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:800; font-size:24px; margin:0 auto 16px; box-shadow:0 4px 12px rgba(0,0,0,0.1); backdrop-filter:blur(4px);">
|
||||
{{ $initials }}
|
||||
</div>
|
||||
|
||||
<h2 style="margin:0 0 4px; color:#fff; font-size:18px; font-weight:700;">{{ $user->name }}</h2>
|
||||
<p style="margin:0 0 16px; color:#e0e7ff; font-size:12px;">{{ $user->email ?? $user->username }}</p>
|
||||
|
||||
<div style="display:inline-flex; align-items:center; gap:6px; background:rgba(255,255,255,0.2); padding:6px 14px; border-radius:999px; border:1px solid rgba(255,255,255,0.3); color:#fff; font-size:10px; font-weight:800; letter-spacing:0.5px;">
|
||||
<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
{{ strtoupper($user->role) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Stats List --}}
|
||||
<div style="padding:16px;">
|
||||
<div style="display:flex; align-items:center; gap:12px; padding:12px; background:#f8fafc; border-radius:12px; border:1px solid #f1f5f9; margin-bottom:12px;">
|
||||
<div style="color:#64748b;"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg></div>
|
||||
<div>
|
||||
<div style="font-size:10px; font-weight:700; color:#94a3b8; text-transform:uppercase; letter-spacing:0.5px; margin-bottom:2px;">User ID</div>
|
||||
<div style="font-size:13px; font-weight:700; color:#0f172a;">#{{ $user->id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; align-items:center; gap:12px; padding:12px; background:#f8fafc; border-radius:12px; border:1px solid #f1f5f9; margin-bottom:12px;">
|
||||
<div style="color:#10b981;"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg></div>
|
||||
<div>
|
||||
<div style="font-size:10px; font-weight:700; color:#94a3b8; text-transform:uppercase; letter-spacing:0.5px; margin-bottom:2px;">Total Aktivitas</div>
|
||||
<div style="font-size:13px; font-weight:700; color:#0f172a;">{{ $user->activities_count ?? 0 }} kegiatan</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; align-items:center; gap:12px; padding:12px; background:#f8fafc; border-radius:12px; border:1px solid #f1f5f9; margin-bottom:12px;">
|
||||
<div style="color:#8b5cf6;"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg></div>
|
||||
<div>
|
||||
<div style="font-size:10px; font-weight:700; color:#94a3b8; text-transform:uppercase; letter-spacing:0.5px; margin-bottom:2px;">Bergabung</div>
|
||||
<div style="font-size:13px; font-weight:700; color:#0f172a;">{{ \Carbon\Carbon::parse($user->created_at)->translatedFormat('d F Y') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; align-items:center; gap:12px; padding:12px; background:#f8fafc; border-radius:12px; border:1px solid #f1f5f9;">
|
||||
<div style="color:#f59e0b;"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg></div>
|
||||
<div>
|
||||
<div style="font-size:10px; font-weight:700; color:#94a3b8; text-transform:uppercase; letter-spacing:0.5px; margin-bottom:2px;">Terakhir Diperbarui</div>
|
||||
<div style="font-size:13px; font-weight:700; color:#0f172a;">{{ \Carbon\Carbon::parse($user->updated_at ?? $user->created_at)->translatedFormat('d F Y') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Warning Box --}}
|
||||
<div style="margin:0 16px 16px; padding:14px; background:#fefce8; border:1px solid #fef08a; border-radius:12px; display:flex; gap:10px; align-items:flex-start;">
|
||||
<div style="color:#b45309; flex-shrink:0;">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
||||
<line x1="12" y1="9" x2="12" y2="13"/>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="font-size:11px; color:#b45309; line-height:1.5;">
|
||||
Perubahan data akan langsung berpengaruh pada akses sistem pengguna ini.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- FORM MAIN CONTENT --}}
|
||||
<div style="background:#fff; border-radius:16px; border:1px solid #e2e8f0; overflow:hidden; box-shadow:0 4px 6px -1px rgba(0,0,0,0.05);">
|
||||
<div style="padding:20px 28px; border-bottom:1px solid #f1f5f9; display:flex; align-items:center; gap:12px;">
|
||||
<div style="width:36px; height:36px; background:#eff6ff; border-radius:10px; display:flex; align-items:center; justify-content:center; color:#3b82f6;">
|
||||
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style="margin:0 0 4px; font-size:15px; font-weight:700; color:#0f172a;">Formulir Edit Pengguna</h3>
|
||||
<p style="margin:0; font-size:12px; color:#94a3b8;">Perbarui informasi akun pengguna di bawah ini</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('admin.superadmin.user.update', $user->id) }}" method="POST" style="padding:28px;">
|
||||
@csrf
|
||||
|
||||
{{-- INFORMASI DASAR --}}
|
||||
<div style="margin-bottom:28px;">
|
||||
<div style="display:flex; align-items:center; gap:8px; margin-bottom:16px;">
|
||||
<div style="width:6px; height:6px; background:#3b82f6; border-radius:50%;"></div>
|
||||
<h4 style="margin:0; font-size:12px; font-weight:800; color:#3b82f6; letter-spacing:1px;">INFORMASI DASAR</h4>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<div>
|
||||
<label style="display:block; font-size:12px; font-weight:600; color:#475569; margin-bottom:8px;">
|
||||
Nama Lengkap <span style="color:#ef4444;">*</span>
|
||||
</label>
|
||||
<input type="text" name="name" value="{{ old('name', $user->name) }}"
|
||||
style="width:100%; padding:10px 14px; border:1px solid #e2e8f0; border-radius:8px; font-size:14px; color:#0f172a; outline:none; box-sizing:border-box; background:#f8fafc;"
|
||||
onfocus="this.style.borderColor='#3b82f6'; this.style.background='#fff'"
|
||||
onblur="this.style.borderColor='#e2e8f0'; this.style.background='#f8fafc'" required>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block; font-size:12px; font-weight:600; color:#475569; margin-bottom:8px;">
|
||||
Username <span style="color:#ef4444;">*</span>
|
||||
</label>
|
||||
<input type="text" name="username" value="{{ old('username', $user->username) }}"
|
||||
style="width:100%; padding:10px 14px; border:1px solid #e2e8f0; border-radius:8px; font-size:14px; color:#0f172a; outline:none; box-sizing:border-box; background:#f8fafc;"
|
||||
onfocus="this.style.borderColor='#3b82f6'; this.style.background='#fff'"
|
||||
onblur="this.style.borderColor='#e2e8f0'; this.style.background='#f8fafc'" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{{-- ROLE & HAK AKSES --}}
|
||||
<div style="margin-bottom:28px;">
|
||||
<div style="display:flex; align-items:center; gap:8px; margin-bottom:16px;">
|
||||
<div style="width:6px; height:6px; background:#3b82f6; border-radius:50%;"></div>
|
||||
<h4 style="margin:0; font-size:12px; font-weight:800; color:#3b82f6; letter-spacing:1px;">ROLE & HAK AKSES</h4>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="display:block; font-size:12px; font-weight:600; color:#475569; margin-bottom:8px;">
|
||||
Role Pengguna <span style="color:#ef4444;">*</span>
|
||||
</label>
|
||||
<select name="role"
|
||||
style="width:100%; padding:10px 14px; border:1px solid #e2e8f0; border-radius:8px; font-size:14px; color:#0f172a; outline:none; box-sizing:border-box; background:#f8fafc; appearance:none;"
|
||||
onfocus="this.style.borderColor='#3b82f6'; this.style.background='#fff'"
|
||||
onblur="this.style.borderColor='#e2e8f0'; this.style.background='#f8fafc'" required>
|
||||
<option value="superadmin" {{ old('role', $user->role) == 'superadmin' ? 'selected' : '' }}>Super Admin</option>
|
||||
<option value="admin" {{ old('role', $user->role) == 'admin' ? 'selected' : '' }}>Administrator</option>
|
||||
<option value="user" {{ old('role', $user->role) == 'user' ? 'selected' : '' }}>User</option>
|
||||
</select>
|
||||
<p style="margin:6px 0 0; font-size:11px; color:#94a3b8; display:flex; align-items:center; gap:4px;">
|
||||
<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
||||
Role menentukan level akses pengguna di dalam sistem
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- GANTI PASSWORD --}}
|
||||
<div style="margin-bottom:32px;">
|
||||
<div style="display:flex; align-items:center; gap:8px; margin-bottom:16px;">
|
||||
<div style="width:6px; height:6px; background:#3b82f6; border-radius:50%;"></div>
|
||||
<h4 style="margin:0; font-size:12px; font-weight:800; color:#3b82f6; letter-spacing:1px;">GANTI PASSWORD <span style="font-weight:500; color:#94a3b8; text-transform:none; letter-spacing:0;">(Kosongkan jika tidak ingin mengubah)</span></h4>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<div>
|
||||
<div style="display:flex; align-items:center; gap:6px; margin-bottom:8px;">
|
||||
<label style="font-size:12px; font-weight:600; color:#475569;">
|
||||
Password Baru
|
||||
</label>
|
||||
<span style="background:#f1f5f9; color:#64748b; padding:2px 6px; border-radius:4px; font-size:10px;">Opsional</span>
|
||||
</div>
|
||||
<div style="position:relative;">
|
||||
<input type="password" name="password" id="edit_password"
|
||||
style="width:100%; padding:10px 40px 10px 14px; border:1px solid #e2e8f0; border-radius:8px; font-size:14px; color:#0f172a; outline:none; box-sizing:border-box; background:#f8fafc;"
|
||||
onfocus="this.style.borderColor='#3b82f6'; this.style.background='#fff'"
|
||||
onblur="this.style.borderColor='#e2e8f0'; this.style.background='#f8fafc'"
|
||||
placeholder="Min. 8 karakter">
|
||||
<button type="button" onclick="togglePassword('edit_password', this)"
|
||||
style="position:absolute; right:12px; top:50%; transform:translateY(-50%); background:none; border:none; cursor:pointer; color:#94a3b8; padding:0; display:flex; align-items:center; justify-content:center;">
|
||||
<svg class="eye-icon" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
<svg class="eye-off-icon" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="display:none;">
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/>
|
||||
<line x1="1" y1="1" x2="23" y2="23"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="display:flex; align-items:center; gap:6px; margin-bottom:8px;">
|
||||
<label style="font-size:12px; font-weight:600; color:#475569;">
|
||||
Konfirmasi Password
|
||||
</label>
|
||||
<span style="background:#f1f5f9; color:#64748b; padding:2px 6px; border-radius:4px; font-size:10px;">Opsional</span>
|
||||
</div>
|
||||
<div style="position:relative;">
|
||||
<input type="password" name="password_confirmation" id="edit_password_confirmation"
|
||||
style="width:100%; padding:10px 40px 10px 14px; border:1px solid #e2e8f0; border-radius:8px; font-size:14px; color:#0f172a; outline:none; box-sizing:border-box; background:#f8fafc;"
|
||||
onfocus="this.style.borderColor='#3b82f6'; this.style.background='#fff'"
|
||||
onblur="this.style.borderColor='#e2e8f0'; this.style.background='#f8fafc'"
|
||||
placeholder="Ulangi password baru">
|
||||
<button type="button" onclick="togglePassword('edit_password_confirmation', this)"
|
||||
style="position:absolute; right:12px; top:50%; transform:translateY(-50%); background:none; border:none; cursor:pointer; color:#94a3b8; padding:0; display:flex; align-items:center; justify-content:center;">
|
||||
<svg class="eye-icon" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
<svg class="eye-off-icon" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="display:none;">
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/>
|
||||
<line x1="1" y1="1" x2="23" y2="23"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- FOOTER BUTTONS --}}
|
||||
<div class="form-actions">
|
||||
<div style="font-size:12px; color:#64748b; display:flex; align-items:center; gap:6px;">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
||||
Field bertanda <span style="color:#ef4444;">*</span> wajib diisi
|
||||
</div>
|
||||
<div style="display:flex; gap:12px;">
|
||||
<a href="{{ route('admin.superadmin.user.index') }}"
|
||||
style="padding:10px 24px; border:1px solid #e2e8f0; border-radius:8px; font-size:13px; font-weight:700; color:#475569; text-decoration:none; background:#fff; transition:all .2s;"
|
||||
onmouseover="this.style.background='#f8fafc'" onmouseout="this.style.background='#fff'">
|
||||
Batal
|
||||
</a>
|
||||
<button type="submit"
|
||||
style="padding:10px 24px; background:#2563eb; color:#fff; border:none; border-radius:8px; font-size:13px; font-weight:700; cursor:pointer; display:flex; align-items:center; gap:8px; transition:opacity .2s;"
|
||||
onmouseover="this.style.opacity='.85'" onmouseout="this.style.opacity='1'">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
||||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
|
||||
<polyline points="17 21 17 13 7 13 7 21"/>
|
||||
<polyline points="7 3 7 8 15 8"/>
|
||||
</svg>
|
||||
Simpan Perubahan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
<script>
|
||||
function togglePassword(inputId, buttonElement) {
|
||||
const input = document.getElementById(inputId);
|
||||
const eyeIcon = buttonElement.querySelector('.eye-icon');
|
||||
const eyeOffIcon = buttonElement.querySelector('.eye-off-icon');
|
||||
|
||||
if (input.type === 'password') {
|
||||
input.type = 'text';
|
||||
eyeIcon.style.display = 'none';
|
||||
eyeOffIcon.style.display = 'block';
|
||||
buttonElement.style.color = '#3b82f6';
|
||||
} else {
|
||||
input.type = 'password';
|
||||
eyeIcon.style.display = 'block';
|
||||
eyeOffIcon.style.display = 'none';
|
||||
buttonElement.style.color = '#94a3b8';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,328 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="page-container" style="padding:28px 32px; background:#f1f5f9; min-height:100vh; font-family:'Figtree','Segoe UI',sans-serif;">
|
||||
<style>
|
||||
.hover-card {
|
||||
transition: all .3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hover-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 18px 35px rgba(15, 23, 42, .12);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-container { padding: 16px 12px !important; }
|
||||
.header-flex { flex-direction: column !important; gap: 16px !important; }
|
||||
.stats-grid { grid-template-columns: 1fr !important; }
|
||||
.toolbar-flex { flex-direction: column !important; align-items: stretch !important; gap: 16px !important; }
|
||||
.toolbar-right { flex-direction: column !important; align-items: stretch !important; }
|
||||
.search-input { width: 100% !important; }
|
||||
.btn-tambah { justify-content: center !important; }
|
||||
.header-badges { flex-wrap: wrap; }
|
||||
.table-wrap table { min-width: 800px !important; }
|
||||
}
|
||||
</style>
|
||||
{{-- HEADER --}}
|
||||
<div class="header-flex" style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:24px;">
|
||||
<div>
|
||||
<p style="font-size:11px; font-weight:700; color:#94a3b8; letter-spacing:1.8px; margin:0 0 6px; text-transform:uppercase;">
|
||||
Panel Kendali Sistem
|
||||
</p>
|
||||
<h1 style="font-size:30px; font-weight:800; color:#0f172a; margin:0 0 4px;">
|
||||
Manajemen Pengguna
|
||||
</h1>
|
||||
<p style="margin:0; color:#94a3b8; font-size:13px;">
|
||||
Kelola seluruh akun dan hak akses pengguna sistem
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="header-badges" style="display:flex; align-items:center; gap:10px;">
|
||||
<div style="background:#fff; border:1px solid #e2e8f0; padding:9px 14px; border-radius:10px; font-size:13px; color:#64748b; display:flex; align-items:center; gap:6px;">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" />
|
||||
<line x1="16" y1="2" x2="16" y2="6" />
|
||||
<line x1="8" y1="2" x2="8" y2="6" />
|
||||
<line x1="3" y1="10" x2="21" y2="10" />
|
||||
</svg>
|
||||
{{ now()->translatedFormat('l, d F Y') }}
|
||||
</div>
|
||||
<div style="background:#eef2ff; color:#4338ca; padding:9px 14px; border-radius:10px; font-size:12px; font-weight:700; display:flex; align-items:center; gap:6px;">
|
||||
<div style="width:8px; height:8px; background:#4338ca; border-radius:50%;"></div>
|
||||
{{ strtoupper(session('user')['role'] ?? 'ADMINISTRATOR') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- STATISTIC CARDS --}}
|
||||
<div class="stats-grid" style="display:grid; grid-template-columns:repeat(3,1fr); gap:16px; margin-bottom:24px;">
|
||||
|
||||
{{-- Card 1 --}}
|
||||
<a href="{{ route('admin.superadmin.user.index') }}"
|
||||
class="hover-card"
|
||||
style="text-decoration:none; display:block; background:#fff; border-radius:16px; padding:22px 24px; border:1px solid #e2e8f0; position:relative; overflow:hidden;">
|
||||
<div style="position:absolute; top:0; left:0; right:0; height:3px; background:#3b82f6;"></div>
|
||||
<div style="display:flex; justify-content:space-between; align-items:flex-start;">
|
||||
<div>
|
||||
<p style="font-size:11px; font-weight:700; color:#94a3b8; letter-spacing:1.2px; margin:0 0 10px; text-transform:uppercase;">
|
||||
Total Pengguna
|
||||
</p>
|
||||
<h2 style="font-size:36px; font-weight:800; margin:0 0 4px; color:#0f172a;">
|
||||
{{ $stats['total'] }}
|
||||
</h2>
|
||||
<p style="font-size:12px; color:#94a3b8; margin:0;">Seluruh akun terdaftar</p>
|
||||
</div>
|
||||
<div style="width:44px; height:44px; background:#eff6ff; border-radius:12px; display:flex; align-items:center; justify-content:center;">
|
||||
<svg width="20" height="20" fill="none" stroke="#3b82f6" stroke-width="2" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="8" r="4" />
|
||||
<path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{{-- Card 2 --}}
|
||||
<a href="{{ route('admin.superadmin.user.index') }}"
|
||||
class="hover-card"
|
||||
style="text-decoration:none; display:block; background:#fff; border-radius:16px; padding:22px 24px; border:1px solid #e2e8f0; position:relative; overflow:hidden;">
|
||||
<div style="position:absolute; top:0; left:0; right:0; height:3px; background:#10b981;"></div>
|
||||
<div style="display:flex; justify-content:space-between; align-items:flex-start;">
|
||||
<div>
|
||||
<p style="font-size:11px; font-weight:700; color:#94a3b8; letter-spacing:1.2px; margin:0 0 10px; text-transform:uppercase;">
|
||||
Pengguna Aktif
|
||||
</p>
|
||||
<h2 style="font-size:36px; font-weight:800; margin:0 0 4px; color:#0f172a;">
|
||||
{{ $stats['active'] }}
|
||||
</h2>
|
||||
<p style="font-size:12px; color:#94a3b8; margin:0;">Memiliki aktivitas tercatat</p>
|
||||
</div>
|
||||
<div style="width:44px; height:44px; background:#f0fdf4; border-radius:12px; display:flex; align-items:center; justify-content:center;">
|
||||
<svg width="20" height="20" fill="none" stroke="#10b981" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
||||
<polyline points="22 4 12 14.01 9 11.01" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{{-- Card 3 --}}
|
||||
<div class="hover-card" style="background:#fff; border-radius:16px; padding:22px 24px; border:1px solid #e2e8f0; position:relative; overflow:hidden;">
|
||||
<div style="position:absolute; top:0; left:0; right:0; height:3px; background:#8b5cf6;"></div>
|
||||
<div style="display:flex; justify-content:space-between; align-items:flex-start;">
|
||||
<div>
|
||||
<p style="font-size:11px; font-weight:700; color:#94a3b8; letter-spacing:1.2px; margin:0 0 10px; text-transform:uppercase;">
|
||||
Total Aktivitas
|
||||
</p>
|
||||
<h2 style="font-size:36px; font-weight:800; margin:0 0 4px; color:#0f172a;">
|
||||
{{ $stats['activities'] }}
|
||||
</h2>
|
||||
<p style="font-size:12px; color:#94a3b8; margin:0;">Dari seluruh pengguna</p>
|
||||
</div>
|
||||
<div style="width:44px; height:44px; background:#f5f3ff; border-radius:12px; display:flex; align-items:center; justify-content:center;">
|
||||
<svg width="20" height="20" fill="none" stroke="#8b5cf6" stroke-width="2" viewBox="0 0 24 24">
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- TABLE CARD --}}
|
||||
<div style="background:#fff; border-radius:16px; border:1px solid #e2e8f0; overflow:hidden;">
|
||||
|
||||
{{-- TABLE TOOLBAR --}}
|
||||
<div class="toolbar-flex" style="padding:18px 24px; border-bottom:1px solid #f1f5f9; display:flex; justify-content:space-between; align-items:center; gap:12px;">
|
||||
|
||||
<div style="display:flex; align-items:center; gap:10px;">
|
||||
<div style="width:32px; height:32px; background:#eff6ff; border-radius:8px; display:flex; align-items:center; justify-content:center;">
|
||||
<svg width="16" height="16" fill="none" stroke="#2563eb" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style="margin:0; font-size:15px; font-weight:700; color:#0f172a;">Daftar Pengguna Sistem</h3>
|
||||
</div>
|
||||
<span style="background:#eff6ff; color:#2563eb; padding:4px 10px; border-radius:999px; font-size:12px; font-weight:700;">
|
||||
Total: {{ $stats['total'] }} Pengguna
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-right" style="display:flex; align-items:center; gap:10px;">
|
||||
{{-- SEARCH --}}
|
||||
<div style="position:relative; flex:1;">
|
||||
<svg style="position:absolute; left:10px; top:50%; transform:translateY(-50%); color:#94a3b8;" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
<input type="text" placeholder="Cari nama atau username..." class="search-input"
|
||||
style="padding:9px 12px 9px 32px; border:1px solid #e2e8f0; border-radius:10px; font-size:13px; color:#475569; outline:none; width:220px; background:#f8fafc; box-sizing:border-box;">
|
||||
</div>
|
||||
|
||||
{{-- TAMBAH --}}
|
||||
<a href="{{ route('admin.superadmin.user.create') }}" class="btn-tambah"
|
||||
style="background:#2563eb; color:#fff; text-decoration:none; padding:9px 16px; border-radius:10px; font-size:13px; font-weight:700; display:flex; align-items:center; gap:6px; white-space:nowrap; box-sizing:border-box;">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
Tambah Pengguna
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- TABLE --}}
|
||||
<div class="table-wrap" style="overflow-x:auto;">
|
||||
<table style="width:100%; border-collapse:collapse; min-width:800px;">
|
||||
<thead>
|
||||
<tr style="background:#f8fafc;">
|
||||
<th style="padding:13px 16px; text-align:left; font-size:11px; font-weight:700; color:#94a3b8; letter-spacing:.8px; width:40px;">#</th>
|
||||
<th style="padding:13px 16px; text-align:left; font-size:11px; font-weight:700; color:#94a3b8; letter-spacing:.8px;">PENGGUNA</th>
|
||||
<th style="padding:13px 16px; text-align:left; font-size:11px; font-weight:700; color:#94a3b8; letter-spacing:.8px;">USERNAME</th>
|
||||
<th style="padding:13px 16px; text-align:left; font-size:11px; font-weight:700; color:#94a3b8; letter-spacing:.8px;">ROLE</th>
|
||||
<th style="padding:13px 16px; text-align:left; font-size:11px; font-weight:700; color:#94a3b8; letter-spacing:.8px;">KEGIATAN</th>
|
||||
<th style="padding:13px 16px; text-align:left; font-size:11px; font-weight:700; color:#94a3b8; letter-spacing:.8px;">TERDAFTAR</th>
|
||||
<th style="padding:13px 16px; text-align:center; font-size:11px; font-weight:700; color:#94a3b8; letter-spacing:.8px;">AKSI</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($users as $index => $user)
|
||||
<tr style="border-top:1px solid #f1f5f9; transition:background .15s;" onmouseover="this.style.background='#fafbff'" onmouseout="this.style.background='transparent'">
|
||||
|
||||
{{-- NO --}}
|
||||
<td style="padding:16px; color:#94a3b8; font-size:13px; font-weight:600;">
|
||||
{{ $loop->iteration }}
|
||||
</td>
|
||||
|
||||
{{-- USER --}}
|
||||
<td style="padding:16px;">
|
||||
<div style="display:flex; align-items:center; gap:11px;">
|
||||
@php
|
||||
$colors = ['#2563eb','#7c3aed','#059669','#dc2626','#d97706'];
|
||||
$color = $colors[$user->id % count($colors)];
|
||||
$initials = strtoupper(substr($user->name, 0, 2));
|
||||
@endphp
|
||||
<div style="width:38px; height:38px; border-radius:10px; background:{{ $color }}; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:800; font-size:13px; flex-shrink:0;">
|
||||
{{ $initials }}
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-weight:700; color:#0f172a; font-size:14px;">{{ $user->name }}</div>
|
||||
<div style="font-size:12px; color:#94a3b8;">ID #{{ $user->id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{{-- USERNAME --}}
|
||||
<td style="padding:16px; color:#475569; font-size:13px;">{{ $user->username }}</td>
|
||||
|
||||
{{-- ROLE --}}
|
||||
<td style="padding:16px;">
|
||||
@if($user->role == 'superadmin')
|
||||
<span style="background:#fef9c3; color:#854d0e; padding:5px 12px; border-radius:999px; font-size:12px; font-weight:700;">Super Admin</span>
|
||||
@elseif($user->role == 'admin')
|
||||
<span style="background:#dbeafe; color:#1d4ed8; padding:5px 12px; border-radius:999px; font-size:12px; font-weight:700;">Administrator</span>
|
||||
@else
|
||||
<span style="background:#dcfce7; color:#15803d; padding:5px 12px; border-radius:999px; font-size:12px; font-weight:700;">Petugas</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
{{-- ACTIVITY --}}
|
||||
<td style="padding:16px;">
|
||||
<div style="display:flex; align-items:center; gap:8px;">
|
||||
<span style="font-weight:700; color:#0f172a; font-size:14px;">{{ $user->activities_count }}</span>
|
||||
<div style="height:4px; width:40px; background:#f1f5f9; border-radius:4px; overflow:hidden;">
|
||||
@php $maxAct = max($stats['max_activities'] ?? 1, 1); @endphp
|
||||
<div style="height:100%; background:#3b82f6; border-radius:4px; width:{{ min(($user->activities_count / $maxAct) * 100, 100) }}%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{{-- CREATED --}}
|
||||
<td style="padding:16px; color:#64748b; font-size:13px;">
|
||||
{{ $user->created_at->translatedFormat('d M Y') }}
|
||||
</td>
|
||||
|
||||
{{-- ACTION --}}
|
||||
<td style="padding:16px; text-align:center;">
|
||||
<div style="display:flex; justify-content:center; gap:6px;">
|
||||
@php
|
||||
$editRoute = (session('user')['role'] ?? '') === 'superadmin' ? route('admin.superadmin.user.edit', $user->id) : route('admin.users.edit', $user->id);
|
||||
@endphp
|
||||
<a href="{{ $editRoute }}"
|
||||
style="background:#eff6ff; color:#2563eb; padding:7px 14px; border-radius:8px; text-decoration:none; font-size:12px; font-weight:700; display:inline-flex; align-items:center; gap:4px;">
|
||||
<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||
</svg>
|
||||
Edit
|
||||
</a>
|
||||
|
||||
@if($user->role !== 'superadmin')
|
||||
<form action="{{ route('admin.superadmin.user.destroy', $user->id) }}" method="POST"
|
||||
onsubmit="return confirm('Yakin ingin menghapus pengguna ini?')" style="margin:0;">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit"
|
||||
style="background:#fef2f2; color:#dc2626; padding:7px 14px; border:none; border-radius:8px; font-size:12px; font-weight:700; cursor:pointer; display:inline-flex; align-items:center; gap:4px;">
|
||||
<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||||
<path d="M10 11v6" />
|
||||
<path d="M14 11v6" />
|
||||
<path d="M9 6V4h6v2" />
|
||||
</svg>
|
||||
Hapus
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" style="padding:48px; text-align:center; color:#94a3b8; font-size:14px;">
|
||||
<div style="display:flex; flex-direction:column; align-items:center; gap:8px;">
|
||||
<svg width="36" height="36" fill="none" stroke="#cbd5e1" stroke-width="1.5" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="8" r="4" />
|
||||
<path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" />
|
||||
</svg>
|
||||
Belum ada pengguna terdaftar.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- FOOTER / PAGINATION --}}
|
||||
<div style="padding:14px 24px; border-top:1px solid #f1f5f9; display:flex; justify-content:space-between; align-items:center;">
|
||||
<p style="font-size:13px; color:#94a3b8; margin:0;">
|
||||
Menampilkan 1–{{ $users->count() }} dari {{ $users->count() }} pengguna
|
||||
</p>
|
||||
|
||||
{{-- Pagination (if using paginate()) --}}
|
||||
@if(method_exists($users, 'links'))
|
||||
<div>{{ $users->links() }}</div>
|
||||
@else
|
||||
<div style="display:flex; gap:4px;">
|
||||
<button style="width:32px; height:32px; border:1px solid #e2e8f0; background:#fff; border-radius:8px; font-size:13px; color:#94a3b8; cursor:pointer;"><</button>
|
||||
<button style="width:32px; height:32px; border:1px solid #2563eb; background:#2563eb; border-radius:8px; font-size:13px; color:#fff; font-weight:700; cursor:pointer;">1</button>
|
||||
<button style="width:32px; height:32px; border:1px solid #e2e8f0; background:#fff; border-radius:8px; font-size:13px; color:#94a3b8; cursor:pointer;">></button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,263 @@
|
||||
@extends('user.layouts.app')
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
// Logika status agar sinkron dengan database
|
||||
$status = $activity->status ?? 'on_progres';
|
||||
|
||||
$statusLabel = match($status) {
|
||||
'selesai' => 'DISETUJUI',
|
||||
'on_progres' => 'ON PROGRES',
|
||||
'review' => 'REVIEW',
|
||||
default => strtoupper($status),
|
||||
};
|
||||
|
||||
$statusColor = match($status) {
|
||||
'selesai' => 'bg-green-100 text-green-700',
|
||||
'on_progres' => 'bg-blue-100 text-blue-700',
|
||||
'review' => 'bg-yellow-100 text-yellow-700',
|
||||
default => 'bg-gray-100 text-gray-700',
|
||||
};
|
||||
|
||||
$fileName = $activity->lampiran ? basename($activity->lampiran) : null;
|
||||
$shortName = $fileName && strlen($fileName) > 30
|
||||
? substr($fileName, 0, 30) . '...'
|
||||
: $fileName;
|
||||
@endphp
|
||||
|
||||
<div class="max-w-6xl mx-auto mt-10 px-4 pb-16">
|
||||
|
||||
{{-- HEADER --}}
|
||||
<div class="mb-8">
|
||||
<div class="flex gap-3 items-center mb-3">
|
||||
<span class="{{ $statusColor }} text-xs px-4 py-1.5 rounded-full font-semibold">
|
||||
{{ $statusLabel }}
|
||||
</span>
|
||||
<span class="text-sm text-slate-400">
|
||||
Terakhir diperbarui:
|
||||
{{ $activity->updated_at ? \Carbon\Carbon::parse($activity->updated_at)->translatedFormat('d M Y') : '-' }}
|
||||
</span>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-[#0D215C]">{{ $activity->nama_kegiatan }}</h1>
|
||||
<p class="text-sm text-slate-400">Informasi lengkap aktivitas pegawai</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
|
||||
{{-- KIRI: KONTEN UTAMA --}}
|
||||
<div class="lg:col-span-2 bg-white p-7 rounded-2xl shadow-sm border border-slate-100">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6 mb-8">
|
||||
<div>
|
||||
<p class="text-xs text-slate-400 uppercase tracking-wide mb-1">Nama Pegawai</p>
|
||||
<p class="font-semibold text-lg text-slate-800">{{ $activity->user->name ?? 'User Tidak Ditemukan' }}</p>
|
||||
<p class="text-sm text-slate-400">Username: {{ $activity->user->username ?? '-' }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-xs text-slate-400 uppercase tracking-wide mb-1">Lokasi</p>
|
||||
<p class="font-semibold text-slate-800 flex items-center gap-2">
|
||||
<i data-lucide="map-pin" class="w-4 h-4 text-slate-400"></i>
|
||||
{{ $activity->lokasi ?? '-' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-xs text-slate-400 uppercase tracking-wide mb-1">Tanggal Mulai</p>
|
||||
<p class="font-semibold text-slate-800 flex items-center gap-2">
|
||||
<i data-lucide="calendar" class="w-4 h-4 text-slate-400"></i>
|
||||
{{ $activity->tanggal ? \Carbon\Carbon::parse($activity->tanggal)->translatedFormat('d F Y') : '-' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-xs text-slate-400 uppercase tracking-wide mb-1">Tenggat Waktu</p>
|
||||
<p class="font-semibold text-slate-800 flex items-center gap-2">
|
||||
<i data-lucide="clock" class="w-4 h-4 text-slate-400"></i>
|
||||
@if($activity->tanggal_selesai)
|
||||
@php
|
||||
$isOverdue = \Carbon\Carbon::parse($activity->tanggal_selesai)->isPast() && $activity->status !== 'selesai' && $activity->status !== 'telah_ditinjau';
|
||||
@endphp
|
||||
<span class="{{ $isOverdue ? 'text-red-600 font-bold' : '' }}">
|
||||
{{ \Carbon\Carbon::parse($activity->tanggal_selesai)->translatedFormat('d F Y') }}
|
||||
@if($isOverdue) (Terlambat) @endif
|
||||
</span>
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="mb-6 border-slate-100">
|
||||
|
||||
<div class="mb-8">
|
||||
<p class="text-xs text-slate-400 uppercase tracking-wide mb-2">Deskripsi Detail</p>
|
||||
<div class="text-sm text-slate-600 leading-relaxed text-justify">
|
||||
{{ $activity->deskripsi ?? 'Tidak ada deskripsi tambahan.' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(!empty($activity->status_note))
|
||||
<div class="mb-8 p-4 bg-blue-50 border border-blue-100 rounded-xl">
|
||||
<p class="text-xs text-blue-500 uppercase tracking-wide mb-2 font-semibold">Catatan Status Terakhir</p>
|
||||
<div class="text-sm text-blue-900 leading-relaxed text-justify">
|
||||
{{ $activity->status_note }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 📝 TIMELINE PROGRESS MONITORING --}}
|
||||
<div class="mb-8">
|
||||
<p class="text-xs text-slate-400 uppercase tracking-wide mb-4">Log Monitoring Progress</p>
|
||||
|
||||
@if(isset($activity->progress) && count($activity->progress) > 0)
|
||||
<div class="relative border-l-2 border-indigo-100 ml-3 pl-5 space-y-6">
|
||||
@foreach($activity->progress as $prog)
|
||||
<div class="relative">
|
||||
<div class="absolute -left-[27px] bg-indigo-500 h-3 w-3 rounded-full border-2 border-white top-1"></div>
|
||||
<div class="text-xs text-slate-400 mb-1">
|
||||
{{ \Carbon\Carbon::parse($prog['created_at'])->format('d M Y, H:i') }} •
|
||||
<span class="font-semibold text-indigo-500">{{ $prog['user']['name'] ?? 'Pegawai' }}</span>
|
||||
</div>
|
||||
<div class="text-sm text-slate-700 bg-slate-50 border border-slate-100 p-3 rounded-lg shadow-sm">
|
||||
{{ $prog['note'] }}
|
||||
@if(!empty($prog['attachment']))
|
||||
<div class="mt-3 pt-3 border-t border-slate-200">
|
||||
<a href="http://127.0.0.1:8000/{{ $prog['attachment'] }}" target="_blank"
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 bg-indigo-50 text-indigo-600 text-xs font-semibold rounded-lg hover:bg-indigo-100 transition-colors border border-indigo-100">
|
||||
<i data-lucide="paperclip" class="w-3.5 h-3.5"></i> Lihat Lampiran Bukti
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="text-sm text-slate-500 bg-slate-50 p-4 rounded-xl border border-dashed border-slate-200 text-center">
|
||||
Belum ada log progress yang dicatat untuk kegiatan ini.
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$isPetugas = false;
|
||||
if(isset($activity->petugas)) {
|
||||
foreach($activity->petugas as $p) {
|
||||
if($p['id'] == session('user_id')) {
|
||||
$isPetugas = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
@if($status !== 'selesai' && $isPetugas)
|
||||
<div class="mt-6 border-t border-slate-100 pt-5">
|
||||
<form action="{{ route('activity.progress', $activity->id) }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-2">Tambahkan Log Progress</label>
|
||||
<div class="flex flex-col gap-3">
|
||||
<textarea name="note" required rows="3" placeholder="Contoh: Tim sudah sampai di lokasi survei..." class="w-full bg-slate-50 border border-slate-200 text-slate-800 rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500 transition-all shadow-sm resize-y"></textarea>
|
||||
|
||||
<div class="flex justify-end gap-3 flex-wrap">
|
||||
<label class="cursor-pointer bg-slate-50 border border-slate-200 text-slate-500 hover:text-indigo-600 hover:border-indigo-300 px-5 py-2.5 rounded-xl text-sm font-medium transition-all shadow-sm flex items-center gap-2 whitespace-nowrap">
|
||||
<i data-lucide="image" class="w-4 h-4"></i> Bukti Foto
|
||||
<input type="file" name="attachment" accept="image/*,.pdf" class="hidden" onchange="this.parentElement.querySelector('span').innerText = this.files[0].name.substring(0,15) + '...'">
|
||||
<span class="text-xs text-slate-400"></span>
|
||||
</label>
|
||||
|
||||
<button type="submit" class="bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-2.5 rounded-xl text-sm font-medium shadow-sm shadow-indigo-200 transition-all flex items-center justify-center gap-2 whitespace-nowrap">
|
||||
<i data-lucide="send" class="w-4 h-4"></i> Kirim Log
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- ACTION BUTTONS --}}
|
||||
<div class="flex gap-3 flex-wrap border-t pt-6">
|
||||
@if($status !== 'selesai' && $isPetugas)
|
||||
<a href="{{ route('activity.edit', $activity->id) }}"
|
||||
class="inline-flex items-center gap-2 bg-yellow-500 hover:bg-yellow-600 text-white px-5 py-2.5 rounded-lg text-sm font-medium transition">
|
||||
<i data-lucide="pencil" class="w-4 h-4"></i> Edit Laporan
|
||||
</a>
|
||||
|
||||
<form action="{{ route('activity.selesai', $activity->id) }}" method="POST">
|
||||
@csrf
|
||||
<button type="submit" onclick="return confirm('Tandai kegiatan ini sebagai selesai?')"
|
||||
class="inline-flex items-center gap-2 bg-green-500 hover:bg-green-600 text-white px-5 py-2.5 rounded-lg text-sm font-medium transition">
|
||||
<i data-lucide="check" class="w-4 h-4"></i> Selesaikan Kegiatan
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- KANAN: SIDEBAR DETAIL --}}
|
||||
<div class="space-y-4">
|
||||
{{-- Lampiran --}}
|
||||
<div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-100">
|
||||
<h4 class="font-semibold mb-4 flex items-center gap-2 text-[#0D215C]">
|
||||
<i data-lucide="paperclip" class="w-4 h-4"></i> Lampiran
|
||||
</h4>
|
||||
|
||||
@if($activity->lampiran)
|
||||
<div class="flex items-center gap-3 bg-red-50 border border-red-100 rounded-xl px-4 py-3 mb-4">
|
||||
<div class="w-10 h-10 rounded-lg bg-red-100 flex items-center justify-center">
|
||||
<i data-lucide="image" class="w-5 h-5 text-red-500"></i>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-medium text-slate-700 truncate">{{ $shortName }}</p>
|
||||
<p class="text-xs text-slate-400">File Lampiran</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<a href="{{ rtrim(env('BACKEND_API_URL', 'http://127.0.0.1:8000'), '/api') . '/storage/' . $activity->lampiran }}" target="_blank"
|
||||
class="flex items-center justify-center gap-2 border border-slate-200 hover:bg-slate-50 text-slate-700 py-2 rounded-lg text-sm transition">
|
||||
<i data-lucide="eye" class="w-4 h-4"></i> Lihat
|
||||
</a>
|
||||
<a href="{{ rtrim(env('BACKEND_API_URL', 'http://127.0.0.1:8000'), '/api') . '/storage/' . $activity->lampiran }}" download
|
||||
class="flex items-center justify-center gap-2 border border-slate-200 hover:bg-slate-50 text-slate-700 py-2 rounded-lg text-sm transition">
|
||||
<i data-lucide="download" class="w-4 h-4"></i> Unduh
|
||||
</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-4 border-2 border-dashed border-slate-100 rounded-xl">
|
||||
<p class="text-sm text-slate-400 italic">Tidak ada lampiran</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Metadata --}}
|
||||
<div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-100">
|
||||
<h4 class="font-semibold mb-4 text-[#0D215C]">Meta Data</h4>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex justify-between text-slate-600">
|
||||
<span>ID Laporan</span>
|
||||
<span class="font-mono text-xs">#ACT-{{ str_pad($activity->id, 5, '0', STR_PAD_LEFT) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-slate-600">
|
||||
<span>Metode</span>
|
||||
<span>{{ $activity->metode ?? 'Luring' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="{{ route('activity.list') }}" class="block text-center text-sm text-blue-600 hover:text-blue-800 transition py-2">
|
||||
← Kembali ke Daftar Laporan
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@@ -0,0 +1,94 @@
|
||||
@extends('user.layouts.app')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="max-w-3xl mx-auto mt-10 px-4 pb-20">
|
||||
|
||||
{{-- HEADER --}}
|
||||
<div class="mb-8">
|
||||
<a href="{{ route('activity.detail', $activity->id) }}" class="text-sm text-blue-600 flex items-center gap-2 mb-4 no-underline hover:underline">
|
||||
<i data-lucide="arrow-left" class="w-4 h-4"></i> Kembali ke Detail
|
||||
</a>
|
||||
<h2 class="text-2xl font-bold text-[#0D215C]">
|
||||
Edit Laporan Kegiatan
|
||||
</h2>
|
||||
<p class="text-sm text-slate-400">Perbarui informasi laporan kegiatan Anda di bawah ini.</p>
|
||||
</div>
|
||||
|
||||
{{-- FORM CARD --}}
|
||||
<div class="bg-white p-8 rounded-2xl shadow-sm border border-slate-100">
|
||||
<form action="{{ route('activity.update', $activity->id) }}" method="POST" class="space-y-6">
|
||||
@csrf
|
||||
|
||||
<div>
|
||||
<label class="text-sm font-semibold text-slate-700 block mb-2">Nama Kegiatan</label>
|
||||
<div class="relative">
|
||||
<input type="text" name="nama_kegiatan"
|
||||
value="{{ old('nama_kegiatan', $activity->nama_kegiatan) }}"
|
||||
placeholder="Masukkan nama kegiatan..."
|
||||
class="w-full p-3 rounded-xl bg-slate-50 border border-slate-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 outline-none transition" required>
|
||||
</div>
|
||||
@error('nama_kegiatan') <span class="text-red-500 text-xs mt-1">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="text-sm font-semibold text-slate-700 block mb-2">Tanggal Pelaksanaan</label>
|
||||
<input type="date" name="tanggal"
|
||||
value="{{ old('tanggal', $activity->tanggal) }}"
|
||||
class="w-full p-3 rounded-xl bg-slate-50 border border-slate-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 outline-none transition" required>
|
||||
@error('tanggal') <span class="text-red-500 text-xs mt-1">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-sm font-semibold text-slate-700 block mb-2">Lokasi</label>
|
||||
<input type="text" name="lokasi"
|
||||
value="{{ old('lokasi', $activity->lokasi) }}"
|
||||
placeholder="Contoh: Kantor Dinas..."
|
||||
class="w-full p-3 rounded-xl bg-slate-50 border border-slate-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 outline-none transition" required>
|
||||
@error('lokasi') <span class="text-red-500 text-xs mt-1">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-sm font-semibold text-slate-700 block mb-2">Deskripsi Kegiatan</label>
|
||||
<textarea name="deskripsi" rows="5"
|
||||
placeholder="Jelaskan detail kegiatan yang dilakukan..."
|
||||
class="w-full p-3 rounded-xl bg-slate-50 border border-slate-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 outline-none transition">{{ old('deskripsi', $activity->deskripsi) }}</textarea>
|
||||
@error('deskripsi') <span class="text-red-500 text-xs mt-1">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-sm font-semibold text-slate-700 block mb-2">Catatan Status Laporan (Node Petugas)</label>
|
||||
<textarea name="status_note" rows="3"
|
||||
placeholder="Contoh: Tugas ini sedang berjalan di desa darit..."
|
||||
class="w-full p-3 rounded-xl bg-slate-50 border border-slate-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 outline-none transition">{{ old('status_note', $activity->status_note ?? '') }}</textarea>
|
||||
@error('status_note') <span class="text-red-500 text-xs mt-1">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-4 pt-4 border-t border-slate-100">
|
||||
<button type="submit"
|
||||
class="bg-[#0D215C] hover:bg-[#0a1a47] text-white px-8 py-3 rounded-xl font-bold transition shadow-lg shadow-blue-900/10 flex items-center gap-2">
|
||||
<i data-lucide="save" class="w-4 h-4"></i> Simpan Perubahan
|
||||
</button>
|
||||
|
||||
<a href="{{ route('activity.detail', $activity->id) }}"
|
||||
class="px-8 py-3 border border-slate-200 text-slate-600 rounded-xl font-semibold hover:bg-slate-50 transition">
|
||||
Batal
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,113 @@
|
||||
@extends('user.layouts.app')
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- PAGE TITLE --}}
|
||||
<div style="margin-bottom:28px; display: flex; justify-content: space-between; align-items: flex-end; padding: 0 4px; flex-wrap: wrap; gap: 16px;">
|
||||
<div>
|
||||
<div style="font-size:26px; font-weight:700; color:#0D215C; font-family:'Outfit', sans-serif;">
|
||||
Riwayat Aktivitas
|
||||
</div>
|
||||
<div style="font-size:14px; color:#6b7280; margin-top:6px; font-family:'Outfit', sans-serif;">
|
||||
Daftar seluruh laporan kegiatan yang telah Anda kirimkan.
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ route('input.activity') }}"
|
||||
style="text-decoration:none; background:#1d4ed8; color:white; padding:10px 24px; border-radius:10px; font-size:13px; font-weight:600; transition:.2s; display: flex; align-items: center; gap: 8px;">
|
||||
<i data-lucide="plus" style="width: 16px; height: 16px;"></i>
|
||||
Input Baru
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{-- LIST CONTAINER --}}
|
||||
<div style="display: flex; flex-direction: column; gap: 16px;">
|
||||
|
||||
@forelse($activities as $item)
|
||||
<div class="card" style="padding: 20px; background: white; border-radius: 16px; border: 1px solid #f1f5f9; box-shadow: 0 1px 3px rgba(0,0,0,0.05); transition: .2s;"
|
||||
onmouseover="this.style.borderColor='#1d4ed8'; this.style.transform='translateY(-2px)'"
|
||||
onmouseout="this.style.borderColor='#f1f5f9'; this.style.transform='translateY(0)'">
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px;">
|
||||
|
||||
<div style="display: flex; gap: 16px; align-items: center;">
|
||||
{{-- Icon Box --}}
|
||||
<div style="width: 52px; height: 52px; background: #eff6ff; border-radius: 14px; display: flex; align-items: center; justify-content: center; color: #1d4ed8; flex-shrink: 0;">
|
||||
<i data-lucide="clipboard-list" style="width: 26px; height: 26px;"></i>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style="font-size: 17px; font-weight: 700; color: #1e293b; margin-bottom: 6px;">
|
||||
{{ $item->nama_kegiatan }}
|
||||
</div>
|
||||
<div style="display: flex; gap: 15px; align-items: center; flex-wrap: wrap;">
|
||||
<span style="font-size: 13px; color: #64748b; display: flex; align-items: center; gap: 6px;">
|
||||
<i data-lucide="user" style="width: 14px; height: 14px;"></i>
|
||||
<strong style="color: #475569;">{{ $item->user->name ?? 'Pegawai' }}</strong>
|
||||
</span>
|
||||
<span style="font-size: 13px; color: #64748b; display: flex; align-items: center; gap: 6px;">
|
||||
<i data-lucide="calendar" style="width: 14px; height: 14px;"></i>
|
||||
{{ $item->tanggal ? \Carbon\Carbon::parse($item->tanggal)->translatedFormat('d M Y') : '-' }}
|
||||
@if(!empty($item->tanggal_selesai))
|
||||
- {{ \Carbon\Carbon::parse($item->tanggal_selesai)->translatedFormat('d M Y') }}
|
||||
@endif
|
||||
</span>
|
||||
<span style="font-size: 13px; color: #64748b; display: flex; align-items: center; gap: 6px;">
|
||||
<i data-lucide="map-pin" style="width: 14px; height: 14px;"></i>
|
||||
{{ $item->lokasi ?? '-' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 20px;">
|
||||
{{-- Badge Status --}}
|
||||
@php
|
||||
$status = $item->status ?? 'on_progres';
|
||||
$isSelesai = $status === 'selesai';
|
||||
$isOverdue = !empty($item->tanggal_selesai) && \Carbon\Carbon::parse($item->tanggal_selesai)->isPast() && !$isSelesai && $status !== 'telah_ditinjau';
|
||||
@endphp
|
||||
<span style="padding: 6px 14px; border-radius: 99px; font-size: 11px; font-weight: 700; letter-spacing: 0.02em; text-transform: uppercase;
|
||||
{{ $isSelesai ? 'background:#dcfce7; color:#15803d;' : ($isOverdue ? 'background:#fef2f2; color:#dc2626; border:1px solid #fecaca;' : 'background:#eff6ff; color:#1d4ed8;') }}">
|
||||
{{ $isSelesai ? 'Selesai' : ($isOverdue ? 'Terlambat' : 'On Progres') }}
|
||||
</span>
|
||||
|
||||
{{-- Action Button --}}
|
||||
<a href="{{ route('activity.detail', $item->id) }}"
|
||||
style="text-decoration: none; color: #1d4ed8; font-weight: 600; font-size: 14px; display: flex; align-items: center; gap: 4px; padding: 8px 12px; border-radius: 8px; transition: .2s;"
|
||||
onmouseover="this.style.background='#eff6ff'"
|
||||
onmouseout="this.style.background='transparent'">
|
||||
Detail
|
||||
<i data-lucide="chevron-right" style="width: 18px; height: 18px;"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
{{-- EMPTY STATE --}}
|
||||
<div style="text-align: center; padding: 80px 20px; background: white; border-radius: 20px; border: 2px dashed #e2e8f0;">
|
||||
<div style="background: #f8fafc; width: 72px; height: 72px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 20px;">
|
||||
<i data-lucide="inbox" style="width: 36px; height: 36px; color: #cbd5e1;"></i>
|
||||
</div>
|
||||
<div style="font-size: 18px; font-weight: 700; color: #475569;">Belum Ada Aktivitas</div>
|
||||
<div style="font-size: 14px; color: #94a3b8; margin-top: 6px; max-width: 300px; margin-left: auto; margin-right: auto;">
|
||||
Laporan kegiatan yang Anda kirimkan nantinya akan muncul di daftar ini.
|
||||
</div>
|
||||
<a href="{{ route('input.activity') }}" style="display: inline-block; margin-top: 24px; color: #1d4ed8; font-weight: 600; font-size: 14px; text-decoration: none;">
|
||||
Mulai buat laporan pertama →
|
||||
</a>
|
||||
</div>
|
||||
@endforelse
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Script untuk memastikan icon Lucide muncul --}}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,897 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dashboard User</title>
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--primary: #1d4ed8;
|
||||
--primary-soft: #e0e7ff;
|
||||
--bg: #f4f6fb;
|
||||
--card: #ffffff;
|
||||
--border: #e5e7eb;
|
||||
--text: #111827;
|
||||
--muted: #6b7280;
|
||||
--danger: #dc2626;
|
||||
--success: #16a34a;
|
||||
--warning: #f59e0b;
|
||||
--info: #2563eb;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
display: flex
|
||||
}
|
||||
|
||||
/* ─── SIDEBAR ─── */
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
height: 100vh;
|
||||
background: #fff;
|
||||
padding: 24px 18px;
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 18px
|
||||
}
|
||||
|
||||
.logo img {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
object-fit: contain
|
||||
}
|
||||
|
||||
.logo-text .top,
|
||||
.logo-text .mid {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
color: #111827
|
||||
}
|
||||
|
||||
.logo-text .bot {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
margin-top: 2px
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
margin: 14px 0
|
||||
}
|
||||
|
||||
.menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px
|
||||
}
|
||||
|
||||
.menu a {
|
||||
padding: 11px 13px;
|
||||
border-radius: 10px;
|
||||
text-decoration: none;
|
||||
color: #374151;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 14px;
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
.menu a:hover {
|
||||
background: #f3f4f6
|
||||
}
|
||||
|
||||
.menu a.active {
|
||||
background: var(--primary-soft);
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.menu a svg {
|
||||
flex-shrink: 0;
|
||||
opacity: .7
|
||||
}
|
||||
|
||||
.menu a.active svg {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
.bottom {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 11px 13px;
|
||||
color: #374151;
|
||||
text-decoration: none;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: #f3f4f6
|
||||
}
|
||||
|
||||
.menu-item svg {
|
||||
opacity: .7
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
padding: 11px 13px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--danger);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background: #fef2f2
|
||||
}
|
||||
|
||||
/* ─── MAIN ─── */
|
||||
.main {
|
||||
flex: 1;
|
||||
padding: 28px 32px;
|
||||
overflow-y: auto;
|
||||
height: 100vh
|
||||
}
|
||||
|
||||
/* ─── HEADER ─── */
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.search-wrap {
|
||||
position: relative;
|
||||
width: 340px
|
||||
}
|
||||
|
||||
.search-wrap svg {
|
||||
position: absolute;
|
||||
left: 13px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.search {
|
||||
width: 100%;
|
||||
padding: 10px 14px 10px 40px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
transition: .2s;
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.search:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(29, 78, 216, .08);
|
||||
}
|
||||
|
||||
/* ─── USER ─── */
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #1e3a8a;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ─── NOTIF ─── */
|
||||
.notif {
|
||||
position: relative;
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.notif-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
.notif-btn:hover {
|
||||
background: #f3f4f6
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
border-radius: 999px;
|
||||
padding: 2px 6px;
|
||||
font-weight: 600;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50px;
|
||||
width: 320px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, .1);
|
||||
display: none;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.dropdown.show {
|
||||
display: block
|
||||
}
|
||||
|
||||
.dropdown-header {
|
||||
padding: 13px 16px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dropdown-header a {
|
||||
font-size: 12px;
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
font-weight: 500
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 13px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dropdown-item:last-child {
|
||||
border-bottom: none
|
||||
}
|
||||
|
||||
.dropdown-item.unread {
|
||||
background: #eef2ff
|
||||
}
|
||||
|
||||
.dropdown-empty {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
font-size: 13px
|
||||
}
|
||||
|
||||
/* ─── PAGE TITLE ─── */
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
color: var(--text)
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
margin-bottom: 22px
|
||||
}
|
||||
|
||||
/* ─── CARDS ─── */
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 24px
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
padding: 20px 22px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--border);
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, .06)
|
||||
}
|
||||
|
||||
.card h4 {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: .05em;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 32px;
|
||||
font-weight: 700
|
||||
}
|
||||
|
||||
.card.total h4,
|
||||
.card.total h2 {
|
||||
color: var(--info)
|
||||
}
|
||||
|
||||
.card.proses h4,
|
||||
.card.proses h2 {
|
||||
color: var(--warning)
|
||||
}
|
||||
|
||||
.card.selesai h4,
|
||||
.card.selesai h2 {
|
||||
color: var(--success)
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card.total .card-icon {
|
||||
background: #dbeafe
|
||||
}
|
||||
|
||||
.card.proses .card-icon {
|
||||
background: #fef3c7
|
||||
}
|
||||
|
||||
.card.selesai .card-icon {
|
||||
background: #dcfce7
|
||||
}
|
||||
|
||||
/* ─── LIST ─── */
|
||||
.list {
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
padding: 16px 20px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #fafafa;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.list-header a {
|
||||
font-size: 13px;
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.list-header a:hover {
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
.item:last-child {
|
||||
border-bottom: none
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
background: #f9fafb
|
||||
}
|
||||
|
||||
.item-left strong {
|
||||
display: block;
|
||||
margin-bottom: 3px;
|
||||
font-size: 14px
|
||||
}
|
||||
|
||||
.item-left small {
|
||||
color: var(--muted);
|
||||
font-size: 12px
|
||||
}
|
||||
|
||||
.badge-status {
|
||||
padding: 5px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: #dcfce7;
|
||||
color: var(--success)
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background: #fef3c7;
|
||||
color: #b45309
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
background: #fee2e2;
|
||||
color: #dc2626
|
||||
}
|
||||
|
||||
/* ─── EMPTY STATE ─── */
|
||||
.empty-state {
|
||||
padding: 48px 20px;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.empty-state svg {
|
||||
margin: 0 auto 12px;
|
||||
display: block;
|
||||
opacity: .35
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 14px
|
||||
}
|
||||
|
||||
/* ─── NO RESULT ─── */
|
||||
.no-result {
|
||||
display: none;
|
||||
padding: 32px 20px;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* ─── RESPONSIVE ─── */
|
||||
@media (max-width: 1024px) {
|
||||
.cards { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
body { flex-direction: column; }
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 16px;
|
||||
flex-direction: column;
|
||||
}
|
||||
.logo {
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.divider { display: none; }
|
||||
.menu {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
overflow-x: visible;
|
||||
gap: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.menu a { white-space: nowrap; justify-content: center; flex: 1 1 calc(50% - 8px); text-align: center; font-size: 13px; padding: 10px; }
|
||||
.bottom {
|
||||
flex-direction: row;
|
||||
margin-top: 8px;
|
||||
gap: 8px;
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 12px;
|
||||
}
|
||||
.bottom a, .bottom button { white-space: nowrap; justify-content: center; flex: 1 1 calc(50% - 8px); font-size: 13px; padding: 10px; }
|
||||
|
||||
.main { padding: 20px 16px; height: auto; }
|
||||
.header { flex-direction: column-reverse; align-items: flex-start; gap: 20px; margin-bottom: 20px; }
|
||||
.search-wrap { width: 100%; }
|
||||
.user { width: 100%; justify-content: space-between; }
|
||||
.dropdown { width: 300px; left: -10px; right: auto; top: 55px; max-width: calc(100vw - 32px); }
|
||||
|
||||
.cards { grid-template-columns: 1fr; gap: 12px; }
|
||||
.item { flex-direction: column; align-items: flex-start; gap: 12px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- ═══ SIDEBAR ═══ -->
|
||||
<div class="sidebar">
|
||||
|
||||
<div class="logo">
|
||||
<img src="{{ asset('Kalimantan-Barat-Logo-Vector.png') }}" alt="Logo">
|
||||
<div class="logo-text">
|
||||
<div class="top">DINAS TENAGA KERJA</div>
|
||||
<div class="mid">DAN TRANSMIGRASI</div>
|
||||
<div class="bot">PROVINSI KALIMANTAN BARAT</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="menu">
|
||||
|
||||
<a href="{{ route('user.dashboard') }}"
|
||||
class="{{ request()->routeIs('user.dashboard') ? 'active' : '' }}">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="7" height="7" rx="1" />
|
||||
<rect x="13" y="3" width="8" height="7" rx="1" />
|
||||
<rect x="13" y="13" width="8" height="8" rx="1" />
|
||||
<rect x="3" y="13" width="7" height="8" rx="1" />
|
||||
</svg>
|
||||
Dashboard
|
||||
</a>
|
||||
|
||||
<a href="{{ route('input.activity') }}"
|
||||
class="{{ request()->routeIs('input.activity') ? 'active' : '' }}">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path d="M11 5H6a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2v-5" />
|
||||
<path d="M17.5 3.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 8.5-8.5z" />
|
||||
</svg>
|
||||
Input Aktivitas
|
||||
</a>
|
||||
|
||||
<a href="{{ route('activity.list') }}"
|
||||
class="{{ request()->routeIs('activity.list') ? 'active' : '' }}">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2" />
|
||||
<rect x="9" y="3" width="6" height="4" rx="1" />
|
||||
<path d="M9 12h6M9 16h4" />
|
||||
</svg>
|
||||
Daftar Aktivitas
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="bottom">
|
||||
<a href="{{ route('settings') }}" class="menu-item">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||
</svg>
|
||||
Pengaturan
|
||||
</a>
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button class="logout-btn" type="submit">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||
<polyline points="16 17 21 12 16 7" />
|
||||
<line x1="21" y1="12" x2="9" y2="12" />
|
||||
</svg>
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ═══ MAIN ═══ -->
|
||||
<div class="main">
|
||||
|
||||
<!-- HEADER -->
|
||||
<div class="header">
|
||||
|
||||
<div class="search-wrap">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.35-4.35" />
|
||||
</svg>
|
||||
<input type="text" class="search" placeholder="Cari aktivitas..."
|
||||
oninput="filterAktivitas(this.value)">
|
||||
</div>
|
||||
|
||||
<div class="user">
|
||||
|
||||
<!-- NOTIF -->
|
||||
<div class="notif">
|
||||
<div class="notif-btn" onclick="toggleNotif()">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
@if($unreadCount > 0)
|
||||
<div class="badge">{{ $unreadCount }}</div>
|
||||
@endif
|
||||
|
||||
<div class="dropdown" id="notifDropdown">
|
||||
<div class="dropdown-header">
|
||||
Notifikasi
|
||||
@if(count($notifications) > 0)
|
||||
<a href="#" onclick="markAllAsRead(event)">Tandai semua dibaca</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div id="notifListContainer">
|
||||
@forelse($notifications as $n)
|
||||
<div class="dropdown-item {{ !$n['read_at'] ? 'unread' : '' }}">
|
||||
{{ $n['data']['message'] ?? 'Ada notifikasi baru' }}
|
||||
<br><small style="color:var(--muted); font-size:11px;">{{ \Carbon\Carbon::parse($n['created_at'])->diffForHumans() }}</small>
|
||||
</div>
|
||||
@empty
|
||||
<div class="dropdown-empty">Tidak ada notifikasi</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- USER INFO -->
|
||||
<div>
|
||||
<strong style="font-size:14px">
|
||||
{{ session('user')['name'] ?? 'User' }}
|
||||
</strong><br>
|
||||
|
||||
<small style="color:var(--muted);font-size:12px">Pegawai</small>
|
||||
</div>
|
||||
|
||||
<div class="avatar">
|
||||
{{ strtoupper(substr(session('user')['name'] ?? 'U', 0, 1)) }}
|
||||
</div>
|
||||
|
||||
</div>{{-- end .user --}}
|
||||
|
||||
</div>{{-- end .header --}}
|
||||
|
||||
<!-- PAGE TITLE -->
|
||||
<div class="page-title">Dashboard Monitoring</div>
|
||||
<div class="page-subtitle">Pantau aktivitas kamu</div>
|
||||
|
||||
<!-- CARDS -->
|
||||
<div class="cards">
|
||||
<div class="card total">
|
||||
<div class="card-icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="#2563eb" stroke-width="2">
|
||||
<rect x="3" y="3" width="7" height="7" rx="1" />
|
||||
<rect x="13" y="3" width="8" height="7" rx="1" />
|
||||
<rect x="13" y="13" width="8" height="8" rx="1" />
|
||||
<rect x="3" y="13" width="7" height="8" rx="1" />
|
||||
</svg>
|
||||
</div>
|
||||
<h4>TOTAL</h4>
|
||||
<h2>{{ $activities->count() }}</h2>
|
||||
</div>
|
||||
<div class="card proses">
|
||||
<div class="card-icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="#f59e0b" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
</div>
|
||||
<h4>ON PROGRESS</h4>
|
||||
<h2>{{ $onProgres }}</h2>
|
||||
</div>
|
||||
<div class="card selesai">
|
||||
<div class="card-icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="#16a34a" stroke-width="2">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
||||
<polyline points="22 4 12 14.01 9 11.01" />
|
||||
</svg>
|
||||
</div>
|
||||
<h4>SELESAI</h4>
|
||||
<h2>{{ $selesai }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RECENT ACTIVITIES -->
|
||||
<div class="list">
|
||||
<div class="list-header">
|
||||
<span>Recent Activities</span>
|
||||
<a href="{{ route('activity.list') }}">Lihat Semua →</a>
|
||||
</div>
|
||||
|
||||
<div id="activity-list">
|
||||
@forelse($activities->take(5) as $item)
|
||||
<div class="item" data-name="{{ strtolower($item->nama_kegiatan) }}">
|
||||
<div class="item-left">
|
||||
<strong>{{ $item->nama_kegiatan }}</strong>
|
||||
<small>
|
||||
{{ $item->user->name ?? 'Pegawai' }}
|
||||
·
|
||||
{{ \Carbon\Carbon::parse($item->tanggal ?? $item->created_at)->translatedFormat('d M Y') }}
|
||||
@if(!empty($item->tanggal_selesai))
|
||||
- {{ \Carbon\Carbon::parse($item->tanggal_selesai)->translatedFormat('d M Y') }}
|
||||
@endif
|
||||
|
||||
@if(!empty($item->lokasi))
|
||||
· {{ $item->lokasi }}
|
||||
@endif
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
@php
|
||||
$isOverdue = !empty($item->tanggal_selesai) && \Carbon\Carbon::parse($item->tanggal_selesai)->isPast() && $item->status !== 'selesai' && $item->status !== 'telah_ditinjau';
|
||||
@endphp
|
||||
@if($item->status == 'selesai')
|
||||
<span class="badge-status badge-success">Selesai</span>
|
||||
@elseif(!empty($item->reviewed_at) || $item->status == 'ditinjau' || $item->status == 'telah_ditinjau')
|
||||
<span class="badge-status badge-info">Ditinjau</span>
|
||||
@elseif($item->status == 'ditolak')
|
||||
<span class="badge-status badge-danger">Ditolak</span>
|
||||
@elseif($isOverdue)
|
||||
<span class="badge-status badge-danger" style="background:#fef2f2; color:#dc2626; border-color:#fecaca;">Terlambat</span>
|
||||
@else
|
||||
<span class="badge-status badge-warning">On Progress</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="empty-state">
|
||||
<svg width="44" height="44" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2" />
|
||||
<rect x="9" y="3" width="6" height="4" rx="1" />
|
||||
<path d="M9 12h6M9 16h4" />
|
||||
</svg>
|
||||
<p>Belum ada aktivitas yang diinput.</p>
|
||||
<a href="{{ route('input.activity') }}"
|
||||
style="display:inline-block;margin-top:12px;padding:8px 18px;
|
||||
background:var(--primary);color:#fff;border-radius:8px;
|
||||
text-decoration:none;font-size:13px;font-weight:500">
|
||||
+ Input Aktivitas
|
||||
</a>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<!-- No result saat search -->
|
||||
<div class="no-result" id="no-result">
|
||||
Tidak ada aktivitas yang cocok dengan pencarian.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>{{-- end .list --}}
|
||||
|
||||
</div>{{-- end .main --}}
|
||||
|
||||
<script>
|
||||
// ─── TOGGLE NOTIF ───
|
||||
function toggleNotif() {
|
||||
document.getElementById("notifDropdown").classList.toggle("show");
|
||||
}
|
||||
document.addEventListener('click', function(e) {
|
||||
const notif = document.querySelector('.notif');
|
||||
if (notif && !notif.contains(e.target)) {
|
||||
document.getElementById('notifDropdown').classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
async function markAllAsRead(e) {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await fetch('/user/notifications-api/read', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
// Hapus badge dan ganti class unread
|
||||
const badge = document.querySelector('.badge');
|
||||
if (badge) badge.style.display = 'none';
|
||||
document.querySelectorAll('.dropdown-item.unread').forEach(el => {
|
||||
el.classList.remove('unread');
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── FILTER SEARCH ───
|
||||
function filterAktivitas(keyword) {
|
||||
const items = document.querySelectorAll('#activity-list .item');
|
||||
const noResult = document.getElementById('no-result');
|
||||
keyword = keyword.toLowerCase().trim();
|
||||
let visibleCount = 0;
|
||||
|
||||
items.forEach(item => {
|
||||
const name = item.getAttribute('data-name') || '';
|
||||
if (name.includes(keyword)) {
|
||||
item.style.display = 'flex';
|
||||
visibleCount++;
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
if (visibleCount === 0 && items.length > 0) {
|
||||
noResult.style.display = 'block';
|
||||
} else {
|
||||
noResult.style.display = 'none';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,123 @@
|
||||
@extends('user.layouts.app')
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- PAGE TITLE --}}
|
||||
<div style="margin-bottom:28px;">
|
||||
<div style="font-size:26px; font-weight:700; color:#111827; font-family:'Outfit', sans-serif;">
|
||||
Input Laporan Kegiatan
|
||||
</div>
|
||||
<div style="font-size:14px; color:#6b7280; margin-top:6px; font-family:'Outfit', sans-serif;">
|
||||
Pastikan data yang dimasukkan sudah sesuai dengan dokumentasi lapangan.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($errors->any())
|
||||
<div style="background-color: #fee2e2; border-left: 4px solid #ef4444; padding: 16px; margin-bottom: 24px; border-radius: 4px;">
|
||||
<div style="color: #b91c1c; font-weight: 600; margin-bottom: 8px;">Terdapat kesalahan pada input Anda:</div>
|
||||
<ul style="color: #b91c1c; font-size: 14px; margin: 0; padding-left: 20px;">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('activities.store') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
|
||||
{{-- DATA PEGAWAI --}}
|
||||
<div class="card">
|
||||
|
||||
<div style="font-size:13px; font-weight:600; color:#1d4ed8; margin-bottom:16px;
|
||||
padding-bottom:12px; border-bottom:1px solid #e5e7eb; letter-spacing:0.04em;">
|
||||
DATA PEGAWAI
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<div>
|
||||
<label class="label">Nama Pegawai</label>
|
||||
<input type="text" value="{{ session('user')['name'] ?? 'Guest' }}" class="input readonly" readonly>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Username</label>
|
||||
<input type="text" value="{{ session('user')['username'] ?? '-' }}" class="input readonly" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- DETAIL KEGIATAN --}}
|
||||
<div class="card">
|
||||
|
||||
<div style="font-size:13px; font-weight:600; color:#1d4ed8; margin-bottom:16px;
|
||||
padding-bottom:12px; border-bottom:1px solid #e5e7eb; letter-spacing:0.04em;">
|
||||
DETAIL KEGIATAN
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">Judul Kegiatan</label>
|
||||
<input type="text" name="nama_kegiatan" class="input" placeholder="Monitoring perusahaan..." required>
|
||||
</div>
|
||||
|
||||
<div class="form-grid" style="margin-top:16px;">
|
||||
<div>
|
||||
<label class="label">Tanggal</label>
|
||||
<input type="date" name="tanggal" class="input" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Lokasi</label>
|
||||
<input type="text" name="lokasi" class="input" placeholder="Kota / Alamat" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:16px;">
|
||||
<label class="label">Detail Kegiatan</label>
|
||||
<textarea name="deskripsi" class="textarea" placeholder="Tulis laporan lengkap di sini..." required></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- UPLOAD --}}
|
||||
<div class="card">
|
||||
|
||||
<div style="font-size:13px; font-weight:600; color:#1d4ed8; margin-bottom:16px;
|
||||
padding-bottom:12px; border-bottom:1px solid #e5e7eb; letter-spacing:0.04em;">
|
||||
LAMPIRAN DOKUMENTASI
|
||||
</div>
|
||||
|
||||
<label class="upload-box" id="upload-label">
|
||||
<input type="file" name="image" id="file-input" accept=".pdf,.jpg,.jpeg,.png" hidden>
|
||||
|
||||
{{-- Icon Upload --}}
|
||||
<svg width="36" height="36" viewBox="0 0 24 24" fill="none"
|
||||
stroke="#9ca3af" stroke-width="1.5" style="margin:0 auto 10px;">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="17 8 12 3 7 8"/>
|
||||
<line x1="12" y1="3" x2="12" y2="15"/>
|
||||
</svg>
|
||||
|
||||
<p style="font-weight:600; font-size:14px; color:#374151; margin-bottom:4px;" id="file-name">
|
||||
Klik untuk upload file
|
||||
</p>
|
||||
<small style="color:#9ca3af; font-size:12px;">PDF / JPG / PNG · Maksimal 2MB</small>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- SUBMIT --}}
|
||||
<button class="btn" type="submit">
|
||||
Kirim Laporan
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
{{-- Script: tampilkan nama file setelah dipilih --}}
|
||||
<script>
|
||||
document.getElementById('file-input').addEventListener('change', function() {
|
||||
const fileName = this.files[0] ? this.files[0].name : 'Klik untuk upload file';
|
||||
document.getElementById('file-name').textContent = fileName;
|
||||
});
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,215 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Panel Petugas – Disnakertrans Prov. Kalbar</title>
|
||||
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* ─── RESET + FONT ─── */
|
||||
*, *::before, *::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Outfit', sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #F2F5FB;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
/* ─── SCROLLBAR ─── */
|
||||
::-webkit-scrollbar { width: 6px; }
|
||||
::-webkit-scrollbar-track { background: #f1f5f9; }
|
||||
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
|
||||
|
||||
/* ─── VARIABLES ─── */
|
||||
:root {
|
||||
--primary: #1d4ed8;
|
||||
--primary-soft: #e0e7ff;
|
||||
--border: #e5e7eb;
|
||||
--text: #111827;
|
||||
--muted: #6b7280;
|
||||
}
|
||||
|
||||
/* ─── CARD ─── */
|
||||
.card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 28px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* ─── FORM GRID ─── */
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* ─── LABEL ─── */
|
||||
.label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
/* ─── INPUT ─── */
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-family: 'Outfit', sans-serif;
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
transition: .2s;
|
||||
}
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(29,78,216,.1);
|
||||
}
|
||||
.readonly {
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ─── TEXTAREA ─── */
|
||||
.textarea {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-family: 'Outfit', sans-serif;
|
||||
color: var(--text);
|
||||
min-height: 140px;
|
||||
resize: vertical;
|
||||
transition: .2s;
|
||||
}
|
||||
.textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(29,78,216,.1);
|
||||
}
|
||||
|
||||
/* ─── UPLOAD BOX ─── */
|
||||
.upload-box {
|
||||
border: 2px dashed var(--border);
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
transition: .2s;
|
||||
}
|
||||
.upload-box:hover {
|
||||
background: #f9fafb;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
/* ─── BUTTON ─── */
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
font-family: 'Outfit', sans-serif;
|
||||
cursor: pointer;
|
||||
transition: .2s;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.btn:hover {
|
||||
background: #1e40af;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(29,78,216,.3);
|
||||
}
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* ─── RESPONSIVE ─── */
|
||||
@media (max-width: 768px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
main {
|
||||
padding: 20px 0 !important;
|
||||
}
|
||||
.card {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@include('user.layouts.navbar')
|
||||
|
||||
<main style="padding: 40px 0;">
|
||||
<div style="max-width: 860px; margin: auto; padding: 0 24px;">
|
||||
@yield('content')
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('[onclick^="return confirm"]').forEach(function(btn) {
|
||||
const match = btn.getAttribute('onclick').match(/confirm\('([^']+)'\)/);
|
||||
if (match) {
|
||||
const message = match[1];
|
||||
btn.removeAttribute('onclick');
|
||||
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const form = btn.closest('form');
|
||||
|
||||
Swal.fire({
|
||||
title: 'Konfirmasi',
|
||||
text: message,
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3b82f6',
|
||||
cancelButtonColor: '#64748b',
|
||||
confirmButtonText: 'Ya, Lanjutkan',
|
||||
cancelButtonText: 'Batal',
|
||||
reverseButtons: true,
|
||||
customClass: {
|
||||
confirmButton: 'rounded-xl',
|
||||
cancelButton: 'rounded-xl mr-2'
|
||||
}
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
if (form) {
|
||||
form.submit();
|
||||
} else if(btn.getAttribute('href')) {
|
||||
window.location.href = btn.getAttribute('href');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,136 @@
|
||||
<nav class="bg-[#f8fafc] border-b border-slate-200 font-['Outfit'] sticky top-0 z-50 shadow-sm">
|
||||
<div class="max-w-[1200px] mx-auto px-4 md:px-6 lg:px-10">
|
||||
<div class="flex items-center justify-between h-[70px]">
|
||||
<!-- LEFT: LOGO -->
|
||||
<div class="flex items-center gap-2 md:gap-3">
|
||||
<img src="{{ asset('Kalimantan-Barat-Logo-Vector.png') }}" class="h-9 md:h-[42px] w-auto object-contain" alt="Logo Kalbar">
|
||||
<div class="leading-[1.2] border-l-2 border-slate-200 pl-2 md:pl-3">
|
||||
<div class="font-bold text-[11px] md:text-[14px] text-blue-900 tracking-[0.02em]">
|
||||
DINAS TENAGA KERJA DAN TRANSMIGRASI
|
||||
</div>
|
||||
<div class="text-[8px] md:text-[11px] text-slate-500 font-medium">
|
||||
PROVINSI KALIMANTAN BARAT
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hamburger Button (Mobile) -->
|
||||
<div class="flex md:hidden">
|
||||
<button type="button" id="user-menu-btn" class="text-slate-500 hover:text-blue-900 focus:outline-none p-2 rounded-md hover:bg-slate-200 transition-colors">
|
||||
<i data-lucide="menu" class="w-6 h-6"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- CENTER & RIGHT: NAVIGATION (Desktop) -->
|
||||
<div class="hidden md:flex items-center gap-6">
|
||||
<div class="flex items-center gap-2 text-[14px]">
|
||||
<a href="{{ route('user.dashboard') }}"
|
||||
class="flex items-center gap-1.5 px-4 py-2 rounded-lg transition-all duration-200 {{ request()->routeIs('user.dashboard') ? 'text-blue-800 bg-indigo-50 font-semibold' : 'text-slate-500 font-medium hover:bg-slate-100 hover:text-blue-800' }}">
|
||||
<i data-lucide="layout-dashboard" class="w-4 h-4"></i> Dashboard
|
||||
</a>
|
||||
<a href="{{ route('input.activity') }}"
|
||||
class="flex items-center gap-1.5 px-4 py-2 rounded-lg transition-all duration-200 {{ request()->routeIs('input.activity') ? 'text-blue-800 bg-indigo-50 font-semibold' : 'text-slate-500 font-medium hover:bg-slate-100 hover:text-blue-800' }}">
|
||||
<i data-lucide="edit" class="w-4 h-4"></i> Input Kegiatan
|
||||
</a>
|
||||
<a href="{{ route('activity.list') }}"
|
||||
class="flex items-center gap-1.5 px-4 py-2 rounded-lg transition-all duration-200 {{ request()->routeIs('activity.list') ? 'text-blue-800 bg-indigo-50 font-semibold' : 'text-slate-500 font-medium hover:bg-slate-100 hover:text-blue-800' }}">
|
||||
<i data-lucide="folder" class="w-4 h-4"></i> Kegiatan Saya
|
||||
</a>
|
||||
<a href="{{ route('settings') ?? '#' }}"
|
||||
class="flex items-center gap-1.5 px-4 py-2 rounded-lg transition-all duration-200 {{ request()->routeIs('settings') ? 'text-blue-800 bg-indigo-50 font-semibold' : 'text-slate-500 font-medium hover:bg-slate-100 hover:text-blue-800' }}">
|
||||
<i data-lucide="settings" class="w-4 h-4"></i> Pengaturan
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="w-px h-8 bg-slate-300"></div>
|
||||
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="w-[38px] h-[38px] rounded-full bg-blue-900 text-white flex items-center justify-center font-bold text-[16px]">
|
||||
{{ strtoupper(substr(session('user')['name'] ?? 'U', 0, 1)) }}
|
||||
</div>
|
||||
<div class="leading-[1.2]">
|
||||
<div class="font-semibold text-[14px] text-slate-900">
|
||||
{{ session('user')['name'] ?? 'User' }}
|
||||
</div>
|
||||
<div class="text-[11px] text-slate-500 font-medium">
|
||||
USER
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}" class="ml-2 m-0">
|
||||
@csrf
|
||||
<button type="submit" class="bg-transparent border-none cursor-pointer text-red-500 p-1.5 rounded-md hover:bg-red-50 transition-colors" title="Logout">
|
||||
<i data-lucide="log-out" class="w-[18px] h-[18px]"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Panel -->
|
||||
<div id="user-mobile-menu" class="hidden md:hidden bg-[#f8fafc] border-t border-slate-200 shadow-xl absolute w-full left-0 pb-4">
|
||||
<div class="px-4 py-3 flex flex-col gap-2">
|
||||
<a href="{{ route('user.dashboard') }}"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 {{ request()->routeIs('user.dashboard') ? 'text-blue-800 bg-indigo-50 font-semibold' : 'text-slate-600 font-medium hover:bg-slate-200' }}">
|
||||
<i data-lucide="layout-dashboard" class="w-4 h-4"></i> Dashboard
|
||||
</a>
|
||||
<a href="{{ route('input.activity') }}"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 {{ request()->routeIs('input.activity') ? 'text-blue-800 bg-indigo-50 font-semibold' : 'text-slate-600 font-medium hover:bg-slate-200' }}">
|
||||
<i data-lucide="edit" class="w-4 h-4"></i> Input Kegiatan
|
||||
</a>
|
||||
<a href="{{ route('activity.list') }}"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 {{ request()->routeIs('activity.list') ? 'text-blue-800 bg-indigo-50 font-semibold' : 'text-slate-600 font-medium hover:bg-slate-200' }}">
|
||||
<i data-lucide="folder" class="w-4 h-4"></i> Kegiatan Saya
|
||||
</a>
|
||||
<a href="{{ route('settings') ?? '#' }}"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 {{ request()->routeIs('settings') ? 'text-blue-800 bg-indigo-50 font-semibold' : 'text-slate-600 font-medium hover:bg-slate-200' }}">
|
||||
<i data-lucide="settings" class="w-4 h-4"></i> Pengaturan
|
||||
</a>
|
||||
|
||||
<div class="h-px w-full bg-slate-200 my-2"></div>
|
||||
|
||||
<div class="flex items-center justify-between px-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-[38px] h-[38px] rounded-full bg-blue-900 text-white flex items-center justify-center font-bold text-[16px]">
|
||||
{{ strtoupper(substr(session('user')['name'] ?? 'U', 0, 1)) }}
|
||||
</div>
|
||||
<div class="leading-[1.2]">
|
||||
<div class="font-semibold text-[14px] text-slate-900">
|
||||
{{ session('user')['name'] ?? 'User' }}
|
||||
</div>
|
||||
<div class="text-[11px] text-slate-500 font-medium">
|
||||
USER
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}" class="m-0">
|
||||
@csrf
|
||||
<button type="submit" class="bg-transparent border-none cursor-pointer text-red-500 p-2 rounded-md hover:bg-red-50 transition-colors flex items-center gap-2" title="Logout">
|
||||
<i data-lucide="log-out" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const btn = document.getElementById('user-menu-btn');
|
||||
const menu = document.getElementById('user-mobile-menu');
|
||||
if(btn && menu) {
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
menu.classList.toggle('hidden');
|
||||
lucide.createIcons();
|
||||
});
|
||||
document.addEventListener('click', function(e) {
|
||||
if(!menu.contains(e.target) && !btn.contains(e.target)) {
|
||||
menu.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,173 @@
|
||||
@php
|
||||
$role = session('user')['role'] ?? 'user';
|
||||
$layout = in_array($role, ['admin', 'superadmin']) ? 'admin.layouts.app' : 'user.layouts.app';
|
||||
|
||||
if ($role === 'superadmin') {
|
||||
$dashboardRoute = route('admin.superadmin.dashboard');
|
||||
} elseif ($role === 'admin') {
|
||||
$dashboardRoute = route('admin.dashboard');
|
||||
} else {
|
||||
$dashboardRoute = route('user.dashboard');
|
||||
}
|
||||
@endphp
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="max-w-4xl mx-auto mt-10 px-4 pb-20">
|
||||
|
||||
{{-- HEADER --}}
|
||||
<div class="mb-8">
|
||||
<a href="{{ $dashboardRoute }}" class="inline-flex items-center gap-2 text-sm text-slate-500 hover:text-[#0D215C] font-semibold mb-4 transition">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path d="M19 12H5M12 5l-7 7 7 7"/></svg>
|
||||
Kembali ke Dashboard
|
||||
</a>
|
||||
<h2 class="text-2xl font-bold text-[#0D215C]">
|
||||
Pengaturan Profil
|
||||
</h2>
|
||||
<p class="text-sm text-slate-400">Kelola informasi akun dan kata sandi Anda.</p>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="mb-6 p-4 bg-green-100 border border-green-200 text-green-700 rounded-xl text-sm font-medium">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<div class="mb-6 p-4 bg-red-100 border border-red-200 text-red-700 rounded-xl text-sm font-medium">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($errors->any())
|
||||
<div class="mb-6 p-4 bg-red-100 border border-red-200 text-red-700 rounded-xl text-sm font-medium">
|
||||
<ul class="list-disc list-inside">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="grid grid-cols-1 gap-8">
|
||||
|
||||
{{-- FORM INFORMASI PROFIL --}}
|
||||
<div class="bg-white p-8 rounded-2xl shadow-sm border border-slate-100">
|
||||
<h4 class="text-lg font-bold text-[#0D215C] mb-6 flex items-center gap-2">
|
||||
<i data-lucide="user" class="w-5 h-5"></i> Informasi Pribadi
|
||||
</h4>
|
||||
|
||||
<form action="{{ route('settings.update') }}" method="POST" class="space-y-5">
|
||||
@csrf
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="text-sm font-semibold text-slate-700 block mb-2">Nama Lengkap</label>
|
||||
<input type="text" name="name" value="{{ session('user')['name'] ?? '' }}"
|
||||
class="w-full p-3 rounded-xl bg-slate-50 border border-slate-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 outline-none transition" readonly>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-semibold text-slate-700 block mb-2">Username <span class="text-red-500">*</span></label>
|
||||
<input type="text" name="username" value="{{ session('user')['username'] ?? '' }}"
|
||||
class="w-full p-3 rounded-xl bg-slate-50 border border-slate-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 outline-none transition">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="pt-4">
|
||||
<button type="submit" class="bg-[#0D215C] hover:bg-[#0a1a47] text-white px-8 py-3 rounded-xl font-bold transition shadow-lg shadow-blue-900/10">
|
||||
Simpan Perubahan
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@if($role !== 'user')
|
||||
{{-- FORM GANTI PASSWORD --}}
|
||||
<div class="bg-white p-8 rounded-2xl shadow-sm border border-slate-100">
|
||||
<h4 class="text-lg font-bold text-[#0D215C] mb-6 flex items-center gap-2">
|
||||
<i data-lucide="lock" class="w-5 h-5"></i> Keamanan Akun
|
||||
</h4>
|
||||
|
||||
<form action="{{ route('settings.update') }}" method="POST" class="space-y-5">
|
||||
@csrf
|
||||
<input type="hidden" name="username" value="{{ session('user')['username'] ?? '' }}">
|
||||
<div>
|
||||
<label class="text-sm font-semibold text-slate-700 block mb-2">Kata Sandi Saat Ini</label>
|
||||
<div class="relative">
|
||||
<input type="password" name="current_password" id="current_password" autocomplete="off"
|
||||
class="w-full p-3 pr-10 rounded-xl bg-slate-50 border border-slate-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 outline-none transition">
|
||||
<button type="button" onclick="togglePassword('current_password', this)" class="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-blue-500 focus:outline-none">
|
||||
<i data-lucide="eye" class="w-5 h-5 eye-icon"></i>
|
||||
<i data-lucide="eye-off" class="w-5 h-5 eye-off-icon hidden"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="text-sm font-semibold text-slate-700 block mb-2">Kata Sandi Baru</label>
|
||||
<div class="relative">
|
||||
<input type="password" name="password" id="password"
|
||||
class="w-full p-3 pr-10 rounded-xl bg-slate-50 border border-slate-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 outline-none transition">
|
||||
<button type="button" onclick="togglePassword('password', this)" class="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-blue-500 focus:outline-none">
|
||||
<i data-lucide="eye" class="w-5 h-5 eye-icon"></i>
|
||||
<i data-lucide="eye-off" class="w-5 h-5 eye-off-icon hidden"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-semibold text-slate-700 block mb-2">Konfirmasi Kata Sandi</label>
|
||||
<div class="relative">
|
||||
<input type="password" name="password_confirmation" id="password_confirmation"
|
||||
class="w-full p-3 pr-10 rounded-xl bg-slate-50 border border-slate-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 outline-none transition">
|
||||
<button type="button" onclick="togglePassword('password_confirmation', this)" class="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-blue-500 focus:outline-none">
|
||||
<i data-lucide="eye" class="w-5 h-5 eye-icon"></i>
|
||||
<i data-lucide="eye-off" class="w-5 h-5 eye-off-icon hidden"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
<button type="submit" class="bg-white border border-slate-200 hover:bg-slate-50 text-[#0D215C] px-8 py-3 rounded-xl font-bold transition">
|
||||
Perbarui Kata Sandi
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
});
|
||||
|
||||
function togglePassword(inputId, btn) {
|
||||
const input = document.getElementById(inputId);
|
||||
const eye = btn.querySelector('.eye-icon');
|
||||
const eyeOff = btn.querySelector('.eye-off-icon');
|
||||
|
||||
if (input.type === 'password') {
|
||||
input.type = 'text';
|
||||
eye.classList.add('hidden');
|
||||
eyeOff.classList.remove('hidden');
|
||||
btn.classList.add('text-blue-500');
|
||||
btn.classList.remove('text-slate-400');
|
||||
} else {
|
||||
input.type = 'password';
|
||||
eye.classList.remove('hidden');
|
||||
eyeOff.classList.add('hidden');
|
||||
btn.classList.remove('text-blue-500');
|
||||
btn.classList.add('text-slate-400');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote');
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\UserLoginController;
|
||||
use App\Http\Controllers\ActivityController;
|
||||
use App\Http\Controllers\NotificationController;
|
||||
use App\Http\Controllers\Admin\ActivityController as AdminActivityController;
|
||||
use App\Http\Controllers\Admin\SuperAdminController;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| ROUTE UTAMA
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
Route::get('/', function () {
|
||||
return redirect()->route('login');
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| LOGIN & LOGOUT
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
Route::get('/login-user', function () {
|
||||
return view('login-user');
|
||||
})->name('login');
|
||||
|
||||
Route::post('/login-user', [UserLoginController::class, 'login'])
|
||||
->name('login.user');
|
||||
|
||||
Route::post('/logout', [UserLoginController::class, 'logout'])
|
||||
->name('logout');
|
||||
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| AUTH AREA (CUSTOM SESSION LOGIN)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
Route::middleware(['web'])->group(function () {
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| AREA USER (PETUGAS / PENGAWAS)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
Route::middleware(['role:user'])->group(function () {
|
||||
|
||||
// Dashboard Utama User
|
||||
Route::get('/user/dashboard', [ActivityController::class, 'index'])
|
||||
->name('user.dashboard');
|
||||
|
||||
// Form Input Kegiatan
|
||||
Route::get('/activity-input', function () {
|
||||
return view('user.input-activity');
|
||||
})->name('input.activity');
|
||||
|
||||
// Proses Simpan Kegiatan
|
||||
Route::post('/activities/store', [ActivityController::class, 'store'])
|
||||
->name('activities.store');
|
||||
|
||||
// List & Detail Kegiatan
|
||||
Route::get('/activity-list', [ActivityController::class, 'list'])
|
||||
->name('activity.list');
|
||||
|
||||
Route::get('/activity-detail/{id}', [ActivityController::class, 'show'])
|
||||
->name('activity.detail');
|
||||
|
||||
// Edit & Update
|
||||
Route::get('/activity/{id}/edit', [ActivityController::class, 'edit'])
|
||||
->name('activity.edit');
|
||||
|
||||
Route::post('/activity/{id}/update', [ActivityController::class, 'update'])
|
||||
->name('activity.update');
|
||||
|
||||
// Tandai Selesai
|
||||
Route::post('/activity/{id}/selesai', [ActivityController::class, 'selesai'])
|
||||
->name('activity.selesai');
|
||||
|
||||
Route::post('/activity/{id}/progress', [ActivityController::class, 'storeProgress'])
|
||||
->name('activity.progress');
|
||||
});
|
||||
|
||||
// Settings User (Dapat diakses oleh semua role)
|
||||
Route::get('/settings', function () {
|
||||
return view('user.settings');
|
||||
})->name('settings');
|
||||
|
||||
Route::post('/settings/update', [UserLoginController::class, 'updateSettings'])
|
||||
->name('settings.update');
|
||||
|
||||
// Frontend API Proxy untuk Notifikasi
|
||||
Route::get('/user/notifications-api', [NotificationController::class, 'getNotifications']);
|
||||
Route::post('/user/notifications-api/read', [NotificationController::class, 'markAsRead']);
|
||||
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| AREA ADMIN (KASI)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
Route::middleware(['role:admin'])->group(function () {
|
||||
|
||||
// Dashboard Admin
|
||||
Route::get('/admin/dashboard', [ActivityController::class, 'adminDashboard'])
|
||||
->name('admin.dashboard');
|
||||
|
||||
// Monitoring Kegiatan Semua User
|
||||
Route::get('/admin/kegiatan', [AdminActivityController::class, 'index'])
|
||||
->name('admin.kegiatan.index');
|
||||
|
||||
Route::get('/admin/kegiatan/{id}', [AdminActivityController::class, 'show'])
|
||||
->name('admin.kegiatan.show');
|
||||
|
||||
|
||||
|
||||
// Manajemen User (Petugas)
|
||||
Route::get('/admin/users', [UserLoginController::class, 'userList'])
|
||||
->name('admin.users');
|
||||
|
||||
Route::get('/admin/users/create', [UserLoginController::class, 'createUser'])
|
||||
->name('admin.users.create');
|
||||
|
||||
Route::post('/admin/users/store', [UserLoginController::class, 'storeUser'])
|
||||
->name('admin.users.store');
|
||||
|
||||
Route::get('/admin/users/{id}/edit', [UserLoginController::class, 'editUser'])
|
||||
->name('admin.users.edit');
|
||||
|
||||
Route::post('/admin/users/{id}/update', [UserLoginController::class, 'updateUser'])
|
||||
->name('admin.users.update');
|
||||
|
||||
Route::delete('/admin/users/{id}', [UserLoginController::class, 'destroyUser'])
|
||||
->name('admin.users.destroy');
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| AREA SUPER ADMIN (KABID)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
Route::middleware(['role:superadmin'])->group(function () {
|
||||
|
||||
// Dashboard Utama
|
||||
Route::get('/superadmin/dashboard', [SuperAdminController::class, 'index'])
|
||||
->name('admin.superadmin.dashboard');
|
||||
|
||||
// Kegiatan
|
||||
Route::get('/superadmin/kegiatan/create', [SuperAdminController::class, 'createKegiatan'])
|
||||
->name('admin.superadmin.kegiatan.create');
|
||||
|
||||
Route::post('/superadmin/kegiatan/store', [SuperAdminController::class, 'storeKegiatan'])
|
||||
->name('admin.superadmin.kegiatan.store');
|
||||
|
||||
Route::get('/superadmin/kegiatan/{id}', [SuperAdminController::class, 'show'])
|
||||
->name('admin.superadmin.kegiatan.show');
|
||||
|
||||
Route::post('/superadmin/kegiatan/{id}/approve', [SuperAdminController::class, 'approve'])
|
||||
->name('admin.superadmin.kegiatan.approve');
|
||||
|
||||
Route::post('/superadmin/kegiatan/{id}/revision', [SuperAdminController::class, 'revision'])
|
||||
->name('admin.superadmin.kegiatan.revision');
|
||||
|
||||
Route::delete('/superadmin/kegiatan/{id}', [SuperAdminController::class, 'destroy'])
|
||||
->name('admin.superadmin.kegiatan.destroy');
|
||||
|
||||
// Arsip
|
||||
Route::get('/superadmin/arsip', [SuperAdminController::class, 'arsip'])
|
||||
->name('admin.superadmin.arsip');
|
||||
|
||||
Route::post('/superadmin/kegiatan/{id}/archive', [SuperAdminController::class, 'archive'])
|
||||
->name('admin.superadmin.kegiatan.archive');
|
||||
|
||||
// Export
|
||||
Route::post('/superadmin/unduh-laporan', [SuperAdminController::class, 'export'])
|
||||
->name('admin.superadmin.export');
|
||||
|
||||
// User Management
|
||||
Route::get('/superadmin/users', [SuperAdminController::class, 'userIndex'])
|
||||
->name('admin.superadmin.user.index');
|
||||
|
||||
Route::get('/superadmin/users/create', [SuperAdminController::class, 'createUser'])
|
||||
->name('admin.superadmin.user.create');
|
||||
|
||||
Route::post('/superadmin/users/store', [SuperAdminController::class, 'storeUser'])
|
||||
->name('admin.superadmin.user.store');
|
||||
|
||||
Route::get('/superadmin/users/{id}/edit', [SuperAdminController::class, 'editUser'])
|
||||
->name('admin.superadmin.user.edit');
|
||||
|
||||
Route::post('/superadmin/users/{id}/update', [SuperAdminController::class, 'updateUser'])
|
||||
->name('admin.superadmin.user.update');
|
||||
|
||||
Route::delete('/superadmin/users/{id}', [SuperAdminController::class, 'destroyUser'])
|
||||
->name('admin.superadmin.user.destroy');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!private/
|
||||
!public/
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,9 @@
|
||||
compiled.php
|
||||
config.php
|
||||
down
|
||||
events.scanned.php
|
||||
maintenance.php
|
||||
routes.php
|
||||
routes.scanned.php
|
||||
schedule-*
|
||||
services.json
|
||||
@@ -0,0 +1,3 @@
|
||||
*
|
||||
!data/
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
$app = require_once __DIR__ . '/bootstrap/app.php';
|
||||
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
||||
$kernel->bootstrap();
|
||||
|
||||
$token = \Illuminate\Support\Facades\DB::table('personal_access_tokens')->first(); // wait, backend holds tokens, not frontend. Frontend uses session.
|
||||
// So we can't easily test without a token.
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
// use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_the_application_returns_a_successful_response(): void
|
||||
{
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_that_true_is_true(): void
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import laravel from 'laravel-vite-plugin';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
laravel({
|
||||
input: ['resources/css/app.css', 'resources/js/app.js'],
|
||||
refresh: true,
|
||||
}),
|
||||
tailwindcss(),
|
||||
],
|
||||
server: {
|
||||
watch: {
|
||||
ignored: ['**/storage/framework/views/**'],
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user