commit b641245f3e76ddad2760148364d7f9ed828ebd04 Author: Alzando <123951867+allzndd@users.noreply.github.com> Date: Sat May 31 06:29:56 2025 +0700 Jaga Jalan diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8f0de65 --- /dev/null +++ b/.editorconfig @@ -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 + +[docker-compose.yml] +indent_size = 4 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2a4a8b7 --- /dev/null +++ b/.env.example @@ -0,0 +1,64 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_TIMEZONE=UTC +APP_URL=http://localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +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_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=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}" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..46340a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +/.phpunit.cache +/node_modules +/public/build +/public/hot +/public/storage +/storage/*.key +/vendor +.env +.env.backup +.env.production +.phpactor.json +.phpunit.result.cache +Homestead.json +Homestead.yaml +auth.json +npm-debug.log +yarn-error.log +/.fleet +/.idea +/.vscode diff --git a/README.md b/README.md new file mode 100644 index 0000000..1a4c26b --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +
+ + + +## About Laravel + +Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: + +- [Simple, fast routing engine](https://laravel.com/docs/routing). +- [Powerful dependency injection container](https://laravel.com/docs/container). +- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. +- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). +- Database agnostic [schema migrations](https://laravel.com/docs/migrations). +- [Robust background job processing](https://laravel.com/docs/queues). +- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). + +Laravel is accessible, powerful, and provides tools required for large, robust applications. + +## Learning Laravel + +Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. + +You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch. + +If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. + +## Laravel Sponsors + +We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com). + +### Premium Partners + +- **[Vehikl](https://vehikl.com/)** +- **[Tighten Co.](https://tighten.co)** +- **[WebReinvent](https://webreinvent.com/)** +- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** +- **[64 Robots](https://64robots.com)** +- **[Curotec](https://www.curotec.com/services/technologies/laravel/)** +- **[Cyber-Duck](https://cyber-duck.co.uk)** +- **[DevSquad](https://devsquad.com/hire-laravel-developers)** +- **[Jump24](https://jump24.co.uk)** +- **[Redberry](https://redberry.international/laravel/)** +- **[Active Logic](https://activelogic.com)** +- **[byte5](https://byte5.de)** +- **[OP.GG](https://op.gg)** + +## 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). diff --git a/app/Console/Commands/SetupBroadcasting.php b/app/Console/Commands/SetupBroadcasting.php new file mode 100644 index 0000000..18c11ec --- /dev/null +++ b/app/Console/Commands/SetupBroadcasting.php @@ -0,0 +1,103 @@ +info('Setting up broadcasting for real-time notifications...'); + + // 1. Update .env file + $this->updateEnvFile(); + + // 2. Clear configuration cache + $this->call('config:clear'); + + // 3. Clear application cache + $this->call('cache:clear'); + + // 4. Publish vendor files if needed + if ($this->confirm('Do you want to publish vendor files for Laravel Echo and Pusher?', true)) { + $this->call('vendor:publish', ['--provider' => 'Laravel\Ui\UiServiceProvider']); + } + + $this->info('Broadcasting setup completed successfully!'); + $this->info('Remember to set your PUSHER_APP_ID, PUSHER_APP_SECRET in your .env file'); + $this->info('PUSHER_APP_KEY is already set as d82783da6b2fe32e1dec'); + $this->info('PUSHER_APP_CLUSTER is already set as ap1'); + + return 0; + } + + /** + * Update the .env file with broadcasting configuration + */ + protected function updateEnvFile() + { + $envPath = base_path('.env'); + + if (File::exists($envPath)) { + $envContent = File::get($envPath); + + // Check if broadcasting driver is already set + if (strpos($envContent, 'BROADCAST_DRIVER=') === false) { + $this->info('Adding BROADCAST_DRIVER to .env file'); + $envContent .= "\nBROADCAST_DRIVER=pusher\n"; + } else { + $this->info('Updating BROADCAST_DRIVER in .env file'); + $envContent = preg_replace('/BROADCAST_DRIVER=(.*)/i', 'BROADCAST_DRIVER=pusher', $envContent); + } + + // Check if Pusher credentials are already set + if (strpos($envContent, 'PUSHER_APP_KEY=') === false) { + $this->info('Adding Pusher configuration to .env file'); + $envContent .= "PUSHER_APP_ID=\n"; + $envContent .= "PUSHER_APP_KEY=d82783da6b2fe32e1dec\n"; + $envContent .= "PUSHER_APP_SECRET=\n"; + $envContent .= "PUSHER_APP_CLUSTER=ap1\n"; + } else { + $this->info('Updating Pusher configuration in .env file'); + $envContent = preg_replace('/PUSHER_APP_KEY=(.*)/i', 'PUSHER_APP_KEY=d82783da6b2fe32e1dec', $envContent); + $envContent = preg_replace('/PUSHER_APP_CLUSTER=(.*)/i', 'PUSHER_APP_CLUSTER=ap1', $envContent); + } + + File::put($envPath, $envContent); + $this->info('.env file has been updated.'); + } else { + $this->error('.env file does not exist!'); + } + } +} \ No newline at end of file diff --git a/app/Events/LaporanDibuat.php b/app/Events/LaporanDibuat.php new file mode 100644 index 0000000..3cd3ead --- /dev/null +++ b/app/Events/LaporanDibuat.php @@ -0,0 +1,71 @@ +laporan = $laporan; + + // Send notification to all admins immediately + $admins = \App\Models\User::where('tipe_pengguna', 'admin') + ->orWhere('tipe_pengguna', 'super admin') + ->get(); + + foreach ($admins as $admin) { + // Trigger NewNotification event for each admin + event(new NewNotification([ + 'id' => uniqid(), + 'data' => [ + 'message' => 'Laporan baru telah dibuat: ' . ($laporan->jenis_bencana ?? $laporan->jenis_rusak), + 'type' => isset($laporan->jenis_bencana) ? 'bencana' : 'jalan', + 'created_at' => now()->toIso8601String() + ], + 'notifiable_id' => $admin->id, + 'created_at' => now() + ], $admin->id)); + } + } + + /** + * Get the channels the event should broadcast on. + * + * @return \Illuminate\Broadcasting\Channel|array + */ + public function broadcastOn() + { + // Broadcast to public channel + return new Channel('laporan-channel'); + } + + /** + * Get the data to broadcast. + * + * @return array + */ + public function broadcastWith() + { + return [ + 'laporan' => $this->laporan + ]; + } +} \ No newline at end of file diff --git a/app/Events/NewNotification.php b/app/Events/NewNotification.php new file mode 100644 index 0000000..c9ba0aa --- /dev/null +++ b/app/Events/NewNotification.php @@ -0,0 +1,50 @@ +notification = $notification; + $this->userId = $userId; + } + + /** + * Get the channels the event should broadcast on. + * + * @return \Illuminate\Broadcasting\Channel|array + */ + public function broadcastOn() + { + return new PrivateChannel('notifications.' . $this->userId); + } + + /** + * The event's broadcast name. + * + * @return string + */ + public function broadcastAs() + { + return 'notification.received'; + } +} \ No newline at end of file diff --git a/app/Exports/BencanaExport.php b/app/Exports/BencanaExport.php new file mode 100644 index 0000000..acf8802 --- /dev/null +++ b/app/Exports/BencanaExport.php @@ -0,0 +1,51 @@ +data = $data; + } + + public function collection() + { + return $this->data; + } + + public function headings(): array + { + return [ + 'No', + 'Pelapor', + 'Jenis Bencana', + 'Lokasi', + 'Status', + 'Tanggal', + 'Deskripsi' + ]; + } + + public function map($row): array + { + static $index = 0; + $index++; + + return [ + $index, + $row->pelapor, + ucfirst($row->jenis_bencana), + $row->lokasi, + str_replace('_', ' ', ucfirst($row->status)), + \Carbon\Carbon::parse($row->created_at)->format('d M Y H:i'), + $row->deskripsi + ]; + } +} \ No newline at end of file diff --git a/app/Exports/JalanExport.php b/app/Exports/JalanExport.php new file mode 100644 index 0000000..5425036 --- /dev/null +++ b/app/Exports/JalanExport.php @@ -0,0 +1,51 @@ +data = $data; + } + + public function collection() + { + return $this->data; + } + + public function headings(): array + { + return [ + 'No', + 'Pelapor', + 'Jenis Kerusakan', + 'Lokasi', + 'Status', + 'Tanggal', + 'Deskripsi' + ]; + } + + public function map($row): array + { + static $index = 0; + $index++; + + return [ + $index, + $row->pelapor, + ucfirst($row->jenis_rusak), + $row->lokasi, + str_replace('_', ' ', ucfirst($row->status)), + \Carbon\Carbon::parse($row->created_at)->format('d M Y H:i'), + $row->deskripsi + ]; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php new file mode 100644 index 0000000..74fd0ba --- /dev/null +++ b/app/Http/Controllers/AdminController.php @@ -0,0 +1,190 @@ +user(); + // Ambil semua pengguna dengan tipe 'admin' dan 'super admin' dari tabel users + $admins = User::whereIn('tipe_pengguna', ['admin', 'super admin'])->get(); + + // Tambahkan data untuk dropdown + $instansiOptions = User::getInstansiOptions(); + $jabatanOptions = User::getJabatanOptions(); + + return view('admins.index', compact('admins', 'currentAdmin', 'instansiOptions', 'jabatanOptions')); + } + + public function create() + { + return view('admins.create'); + } + + public function store(Request $request) + { + try { + // Cek apakah user adalah super admin + if (Auth::guard('admin')->user()->tipe_pengguna !== 'super admin') { + return response()->json([ + 'success' => false, + 'message' => 'Unauthorized. Hanya Super Admin yang dapat menambahkan admin baru.' + ], 403); + } + + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:users,email', + 'password' => 'required|string|min:8', + 'alamat' => 'required|string', + 'tipe_pengguna' => 'required|in:admin,super admin', + 'instansi' => 'required|in:BPBD,PUPR', + 'jabatan' => 'required|string', + ], [ + 'name.required' => 'Nama harus diisi', + 'email.required' => 'Email harus diisi', + 'email.email' => 'Format email tidak valid', + 'email.unique' => 'Email sudah digunakan', + 'password.required' => 'Password harus diisi', + 'password.min' => 'Password minimal 8 karakter', + 'alamat.required' => 'Alamat harus diisi', + 'tipe_pengguna.required' => 'Tipe pengguna harus diisi', + 'tipe_pengguna.in' => 'Tipe pengguna tidak valid', + 'instansi.required' => 'Instansi harus diisi', + 'instansi.in' => 'Instansi tidak valid', + 'jabatan.required' => 'Jabatan harus diisi', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'errors' => $validator->errors() + ], 422); + } + + // Validasi jabatan sesuai dengan instansi + $jabatanOptions = User::getJabatanOptions($request->instansi); + if (!array_key_exists($request->jabatan, $jabatanOptions)) { + return response()->json([ + 'success' => false, + 'errors' => ['jabatan' => ['Jabatan tidak valid untuk instansi yang dipilih']] + ], 422); + } + + $admin = User::create([ + 'name' => $request->name, + 'email' => $request->email, + 'password' => Hash::make($request->password), + 'alamat' => $request->alamat, + 'tipe_pengguna' => $request->tipe_pengguna, + 'instansi' => $request->instansi, + 'jabatan' => $request->jabatan, + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Admin berhasil ditambahkan', + 'data' => $admin + ]); + + } catch (\Exception $e) { + Log::error('Error saat menambah admin: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Terjadi kesalahan: ' . $e->getMessage() + ], 500); + } + } + + public function edit(User $admin) + { + $currentAdmin = Auth::user(); + if ($admin->id === $currentAdmin->id) { + return redirect()->route('admins.index') + ->with('error', 'Anda tidak dapat mengedit akun Anda sendiri di sini. Gunakan halaman profile.'); + } + return view('admins.edit', compact('admin')); + } + + public function update(Request $request, User $admin) + { + $currentAdmin = Auth::user(); + if ($admin->id === $currentAdmin->id) { + return redirect()->route('admins.index') + ->with('error', 'Anda tidak dapat mengedit akun Anda sendiri di sini. Gunakan halaman profile.'); + } + + $request->validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:admins,email,' . $admin->id, + 'alamat' => 'required|string', + ]); + + User::where('id', $admin->id)->update([ + 'name' => $request->name, + 'email' => $request->email, + 'alamat' => $request->alamat, + ]); + + return redirect()->route('admins.index') + ->with('success', 'Admin berhasil diperbarui.'); + } + + public function destroy($id) + { + try { + // Cek apakah user adalah super admin + if (Auth::guard('admin')->user()->tipe_pengguna !== 'super admin') { + return response()->json([ + 'success' => false, + 'message' => 'Unauthorized. Hanya Super Admin yang dapat menghapus admin.' + ], 403); + } + + $admin = User::findOrFail($id); + + // Prevent deleting self + if ($admin->id === Auth::guard('admin')->id()) { + return response()->json([ + 'success' => false, + 'message' => 'Tidak dapat menghapus akun sendiri' + ], 403); + } + + $admin->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Admin berhasil dihapus' + ]); + + } catch (\Exception $e) { + Log::error('Error saat menghapus admin: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Terjadi kesalahan: ' . $e->getMessage() + ], 500); + } + } + + // Tambahkan method untuk mendapatkan jabatan berdasarkan instansi (untuk AJAX) + public function getJabatan(Request $request) + { + $instansi = $request->instansi; + $jabatanOptions = User::getJabatanOptions($instansi); + + return response()->json([ + 'success' => true, + 'data' => $jabatanOptions + ]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Api/AuthController.php b/app/Http/Controllers/Api/AuthController.php new file mode 100644 index 0000000..e863386 --- /dev/null +++ b/app/Http/Controllers/Api/AuthController.php @@ -0,0 +1,211 @@ +all(), [ + 'name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:users', + 'password' => 'required|string|min:6', + 'alamat' => 'required|string' + // 'telepon' => 'required|string' + ]); + + if ($validator->fails()) { + return response()->json([ + 'status' => 'error', + 'message' => 'Validation error', + 'errors' => $validator->errors() + ], 422); + } + + try { + // Generate token dan log + $verificationToken = Str::random(64); + \Illuminate\Support\Facades\Log::info('Generated token: ' . $verificationToken); + + $user = User::create([ + 'name' => $request->name, + 'email' => $request->email, + 'password' => Hash::make($request->password), + 'alamat' => $request->alamat, + // 'telepon' => $request->telepon, + 'verification_token' => $verificationToken, + 'is_verified' => false, + 'tipe_pengguna' => 'user' + ]); + + \Illuminate\Support\Facades\Log::info('User created with email: ' . $user->email); + \Illuminate\Support\Facades\Log::info('Stored token: ' . $user->verification_token); + + // Generate URL verifikasi + $verificationUrl = url("/api/verify-email/{$verificationToken}"); + $verificationUrl = str_replace('localhost', '10.10.178.83', $verificationUrl); + + \Illuminate\Support\Facades\Log::info('Generated verification URL: ' . $verificationUrl); + + // Kirim email + Mail::to($user->email)->send(new EmailVerification($user, $verificationUrl)); + \Illuminate\Support\Facades\Log::info('Verification email sent to: ' . $user->email); + + return response()->json([ + 'status' => 'success', + 'message' => 'Registrasi berhasil. Silakan cek email Anda untuk verifikasi.', + ], 201); + + } catch (\Exception $e) { + \Illuminate\Support\Facades\Log::error('Registration error: ' . $e->getMessage()); + \Illuminate\Support\Facades\Log::error('Stack trace: ' . $e->getTraceAsString()); + + return response()->json([ + 'status' => 'error', + 'message' => 'Gagal mengirim email verifikasi: ' . $e->getMessage() + ], 500); + } + } + + + + public function login(Request $request) + { + $validator = Validator::make($request->all(), [ + 'email' => 'required|email', + 'password' => 'required|string|min:6', + ]); + + if ($validator->fails()) { + return response()->json([ + 'status' => 'error', + 'message' => 'Validation error', + 'errors' => $validator->errors() + ], 422); + } + + $credentials = $request->only('email', 'password'); + + // Cek apakah email sudah diverifikasi + $user = User::where('email', $request->email)->first(); + + if ($user && !$user->is_verified) { + return response()->json([ + 'status' => 'error', + 'message' => 'Email belum diverifikasi. Silakan cek email Anda untuk verifikasi.', + ], 401); + } + + if (!$token = Auth::guard('api')->attempt($credentials)) { + return response()->json([ + 'status' => 'error', + 'message' => 'Email atau password salah', + ], 401); + } + + return response()->json([ + 'status' => 'success', + 'user' => Auth::guard('api')->user(), + 'token' => $token, + ]); + } + + public function logout() + { + try { + // Ambil token saat ini + $token = JWTAuth::getToken(); + + // Invalidasi token saat ini + JWTAuth::invalidate($token); + + // Jika Anda menggunakan blacklist, Anda bisa menambahkan logika untuk menghapus semua token + // Misalnya, jika Anda menyimpan token di database, Anda bisa menghapusnya di sini + + return response()->json([ + 'status' => 'success', + 'message' => 'Successfully logged out', + ]); + } catch (\Exception $e) { + return response()->json([ + 'status' => 'error', + 'message' => 'Logout failed: ' . $e->getMessage(), + ], 500); + } + } + + public function refresh() + { + return response()->json([ + 'status' => 'success', + 'user' => Auth::guard('api')->user(), + 'token' => JWTAuth::refresh() + ]); + } + + public function loginWithGoogle(Request $request) + { + $validator = Validator::make($request->all(), [ + 'email' => 'required|email', + 'name' => 'required|string', + 'google_id' => 'required|string', + ]); + + if ($validator->fails()) { + return response()->json([ + 'status' => 'error', + 'message' => 'Validation error', + 'errors' => $validator->errors() + ], 422); + } + + try { + // Cek apakah user sudah ada + $user = User::where('email', $request->email)->first(); + + if (!$user) { + // Jika user belum ada, buat user baru + $user = User::create([ + 'name' => $request->name, + 'email' => $request->email, + 'password' => Hash::make(Str::random(16)), // Password acak + 'google_id' => $request->google_id, + 'alamat' => 'Lumajang', + 'is_verified' => true, // Auto verifikasi untuk login Google + 'tipe_pengguna' => 'user' + ]); + } else { + // Jika user sudah ada, update google_id + $user->update([ + 'google_id' => $request->google_id, + 'is_verified' => true + ]); + } + + // Generate token JWT + $token = JWTAuth::fromUser($user); + + return response()->json([ + 'status' => 'success', + 'user' => $user, + 'token' => $token, + ]); + } catch (\Exception $e) { + return response()->json([ + 'status' => 'error', + 'message' => 'Login with Google failed: ' . $e->getMessage() + ], 500); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Api/ForgotPasswordController.php b/app/Http/Controllers/Api/ForgotPasswordController.php new file mode 100644 index 0000000..36ad186 --- /dev/null +++ b/app/Http/Controllers/Api/ForgotPasswordController.php @@ -0,0 +1,119 @@ +all(), [ + 'email' => 'required|email|exists:users,email', + ], [ + 'email.exists' => 'Email tidak terdaftar dalam sistem kami.' + ]); + + if ($validator->fails()) { + return response()->json([ + 'status' => 'error', + 'message' => 'Validation error', + 'errors' => $validator->errors() + ], 422); + } + + try { + // Generate token + $resetToken = Str::random(64); + \Illuminate\Support\Facades\Log::info('Generated reset token: ' . $resetToken); + + // Simpan token ke database + $user = User::where('email', $request->email)->first(); + $user->reset_password_token = $resetToken; + $user->reset_password_expires_at = now()->addHours(24); // Token berlaku 24 jam + $user->save(); + + \Illuminate\Support\Facades\Log::info('Reset token stored for user: ' . $user->email); + + // Buat URL web yang akan redirect ke aplikasi + $resetUrl = route('password.reset.redirect', ['token' => $resetToken, 'email' => $user->email]); + + // Log URL untuk debugging + \Illuminate\Support\Facades\Log::info('Reset URL: ' . $resetUrl); + + // Kirim email + Mail::to($user->email)->send(new ResetPassword($user, $resetUrl)); + \Illuminate\Support\Facades\Log::info('Reset password email sent to: ' . $user->email); + + return response()->json([ + 'status' => 'success', + 'message' => 'Link reset password telah dikirim ke email Anda.', + ]); + + } catch (\Exception $e) { + \Illuminate\Support\Facades\Log::error('Reset password error: ' . $e->getMessage()); + \Illuminate\Support\Facades\Log::error('Stack trace: ' . $e->getTraceAsString()); + + return response()->json([ + 'status' => 'error', + 'message' => 'Gagal mengirim email reset password: ' . $e->getMessage() + ], 500); + } + } + + public function resetPassword(Request $request) + { + $validator = Validator::make($request->all(), [ + 'token' => 'required|string', + 'password' => 'required|string|min:6|confirmed', + ]); + + if ($validator->fails()) { + return response()->json([ + 'status' => 'error', + 'message' => 'Validation error', + 'errors' => $validator->errors() + ], 422); + } + + try { + // Cari user berdasarkan token + $user = User::where('reset_password_token', $request->token) + ->where('reset_password_expires_at', '>', now()) + ->first(); + + if (!$user) { + return response()->json([ + 'status' => 'error', + 'message' => 'Token tidak valid atau sudah kadaluarsa.' + ], 400); + } + + // Update password + $user->password = Hash::make($request->password); + $user->reset_password_token = null; + $user->reset_password_expires_at = null; + $user->save(); + + return response()->json([ + 'status' => 'success', + 'message' => 'Password berhasil diperbarui. Silakan login dengan password baru Anda.' + ]); + + } catch (\Exception $e) { + \Illuminate\Support\Facades\Log::error('Reset password error: ' . $e->getMessage()); + + return response()->json([ + 'status' => 'error', + 'message' => 'Gagal memperbarui password: ' . $e->getMessage() + ], 500); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php new file mode 100644 index 0000000..aa1e620 --- /dev/null +++ b/app/Http/Controllers/AuthController.php @@ -0,0 +1,80 @@ +route('dashboard'); + } + return view('auth.login'); + } + + public function login(Request $request) + { + $request->validate([ + 'username_or_email' => 'required', + 'password' => 'required|string|min:8', + ]); + + $loginField = filter_var($request->username_or_email, FILTER_VALIDATE_EMAIL) ? 'email' : 'name'; + + if (Auth::attempt([$loginField => $request->username_or_email, 'password' => $request->password])) { + return redirect()->intended(route('dashboard')) + ->with('success', 'Selamat datang, ' . Auth::user()->name); + } + + return back() + ->withInput() + ->withErrors([ + 'username_or_email' => 'Username/email atau password salah.', + ]); + } + + public function logout(Request $request) + { + Auth::guard('web')->logout(); + + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect()->route('login') + ->with('success', 'Anda telah berhasil logout.'); + } + + public function showRegisterForm() + { + return view('auth.register'); + } + + public function register(Request $request) + { + $request->validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:users', + 'password' => 'required|string|min:8|confirmed', + 'alamat' => 'required|string', + 'telepon' => 'required|string', + ]); + + User::create([ + 'name' => $request->name, + 'email' => $request->email, + 'password' => Hash::make($request->password), + 'alamat' => $request->alamat, + 'telepon' => $request->telepon, + 'tipe_pengguna' => 'user', + ]); + + return redirect()->route('login') + ->with('success', 'Registrasi berhasil. Silakan login.'); + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..8677cd5 --- /dev/null +++ b/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ +count(); + $menungguBencana = LaporanBencana::where('status', 'menunggu')->count(); + $menunggu = $menungguJalan + $menungguBencana; + + $dalamProsesJalan = LaporanJalan::where('status', 'dalam_proses')->count(); + $dalamProsesBencana = LaporanBencana::where('status', 'dalam_proses')->count(); + $dalamProses = $dalamProsesJalan + $dalamProsesBencana; + + $selesaiJalan = LaporanJalan::where('status', 'selesai')->count(); + $selesaiBencana = LaporanBencana::where('status', 'selesai')->count(); + $selesai = $selesaiJalan + $selesaiBencana; + + // Persentase perubahan dari bulan lalu + $laporanJalanBulanLalu = LaporanJalan::whereMonth('created_at', Carbon::now()->subMonth()->month)->count(); + $laporanBencanaBulanLalu = LaporanBencana::whereMonth('created_at', Carbon::now()->subMonth()->month)->count(); + + $persentaseKenaikanJalan = $laporanJalanBulanLalu > 0 + ? round((($totalLaporanJalan - $laporanJalanBulanLalu) / $laporanJalanBulanLalu) * 100, 1) + : 100; + + $persentasePenurunanBencana = $laporanBencanaBulanLalu > 0 + ? round((($totalLaporanBencana - $laporanBencanaBulanLalu) / $laporanBencanaBulanLalu) * 100, 1) + : 0; + + // Data untuk grafik statistik bulanan + $monthlyData = []; + for ($i = 5; $i >= 0; $i--) { + $date = now()->subMonths($i); + $month = $date->format('M Y'); + + $jalanCount = LaporanJalan::whereYear('created_at', $date->year) + ->whereMonth('created_at', $date->month) + ->count(); + + $bencanaCount = LaporanBencana::whereYear('created_at', $date->year) + ->whereMonth('created_at', $date->month) + ->count(); + + $monthlyData[] = [ + 'bulan' => $month, + 'jalan' => $jalanCount, + 'bencana' => $bencanaCount + ]; + } + + // Data untuk grafik distribusi jenis kerusakan + $distribusiJalan = DB::table('laporan_jalan') + ->select('jenis_rusak', DB::raw('count(*) as total')) + ->groupBy('jenis_rusak') + ->get(); + + $distribusiBencana = DB::table('laporan_bencana') + ->select('jenis_bencana', DB::raw('count(*) as total')) + ->groupBy('jenis_bencana') + ->get(); + + return view('dashboard', compact( + 'totalLaporan', + 'totalLaporanJalan', + 'totalLaporanBencana', + 'menunggu', + 'menungguJalan', + 'menungguBencana', + 'dalamProses', + 'dalamProsesJalan', + 'dalamProsesBencana', + 'selesai', + 'selesaiJalan', + 'selesaiBencana', + 'persentaseKenaikanJalan', + 'persentasePenurunanBencana', + 'monthlyData', + 'distribusiJalan', + 'distribusiBencana' + )); + } catch (\Exception $e) { + return back()->with('error', 'Terjadi kesalahan saat memuat dashboard: ' . $e->getMessage()); + } + } + + public function getLaporanJalan() + { + try { + $laporanJalan = DB::table('laporan_jalan') + ->select('id', 'lokasi', 'latitude', 'longitude', 'jenis_rusak', 'deskripsi', 'foto', 'status') + ->where('status', 'dalam_proses') + ->get(); + + // Transform foto path menjadi URL lengkap + $laporanJalan = $laporanJalan->map(function($item) { + $item->foto_url = $item->foto ? asset('storage/' . $item->foto) : null; + return $item; + }); + + return response()->json([ + 'success' => true, + 'data' => $laporanJalan + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil data laporan jalan', + 'error' => $e->getMessage() + ], 500); + } + } + + public function getLaporanBencana() + { + try { + $laporanBencana = DB::table('laporan_bencana') + ->select('id', 'lokasi', 'latitude', 'longitude', 'jenis_bencana', 'deskripsi', 'foto', 'status') + ->where('status', 'dalam_proses') + ->get(); + + // Transform foto path menjadi URL lengkap + $laporanBencana = $laporanBencana->map(function($item) { + $item->foto_url = $item->foto ? asset('storage/' . $item->foto) : null; + return $item; + }); + + return response()->json([ + 'success' => true, + 'data' => $laporanBencana + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil data laporan bencana', + 'error' => $e->getMessage() + ], 500); + } + } + + public function getRiwayatLaporan() + { + try { + $user_id = Auth::id(); + + // Get laporan bencana yang dilaporkan oleh pengguna + $bencana = DB::table('laporan_bencana') + ->where('user_id', $user_id) + ->select('id', 'lokasi', 'latitude', 'longitude', 'jenis_bencana', 'deskripsi', 'foto', 'status', 'created_at') + ->orderBy('created_at', 'desc') + ->get() + ->map(function($item) { + $item->foto_url = $item->foto ? asset('storage/' . $item->foto) : null; + return $item; + }); + + // Get laporan jalan yang dilaporkan oleh pengguna + $jalan = DB::table('laporan_jalan') + ->where('user_id', $user_id) + ->select('id', 'lokasi', 'latitude', 'longitude', 'jenis_rusak', 'deskripsi', 'foto', 'status', 'created_at') + ->orderBy('created_at', 'desc') + ->get() + ->map(function($item) { + $item->foto_url = $item->foto ? asset('storage/' . $item->foto) : null; + return $item; + }); + + return response()->json([ + 'success' => true, + 'data' => [ + 'bencana' => $bencana, + 'jalan' => $jalan + ] + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil riwayat laporan', + 'error' => $e->getMessage() + ], 500); + } + } +} diff --git a/app/Http/Controllers/ExampleController.php b/app/Http/Controllers/ExampleController.php new file mode 100644 index 0000000..f5e43b9 --- /dev/null +++ b/app/Http/Controllers/ExampleController.php @@ -0,0 +1,45 @@ +toString(); + DB::table('notifications')->insert([ + 'id' => $notificationId, + 'type' => 'App\\Notifications\\SampleNotification', + 'notifiable_type' => 'App\\Models\\User', + 'notifiable_id' => $userId, + 'data' => json_encode([ + 'message' => 'Ini adalah contoh notifikasi real-time', + 'type' => 'jalan', + 'created_at' => Carbon::now()->toIso8601String() + ]), + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + ]); + + // Ambil notifikasi yang baru dibuat + $notification = DB::table('notifications')->where('id', $notificationId)->first(); + + // Kirim event ke Pusher untuk pembaruan real-time + event(new NewNotification($notification, $userId)); + + return response()->json(['success' => true, 'message' => 'Notifikasi berhasil dibuat']); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/LaporanBencanaController.php b/app/Http/Controllers/LaporanBencanaController.php new file mode 100644 index 0000000..5646414 --- /dev/null +++ b/app/Http/Controllers/LaporanBencanaController.php @@ -0,0 +1,219 @@ +join('users', 'laporan_bencana.user_id', '=', 'users.id') + ->select('laporan_bencana.*', 'users.name as pelapor') + ->orderBy('created_at', 'desc') + ->get(); + + return view('bencana.index', compact('laporanBencana')); + } + + public function getLaporanBencana() + { + $laporanBencana = DB::table('laporan_bencana') + ->select('latitude', 'longitude', 'status', 'jenis_bencana') + ->whereIn('status', ['menunggu', 'dalam_proses']) + ->get(); + + return response()->json($laporanBencana); + } + + public function updateStatus(Request $request, $id) + { + $status = $request->status; + $allowed_status = ['menunggu', 'dalam_proses', 'selesai']; + + if (!in_array($status, $allowed_status)) { + return response()->json(['success' => false, 'message' => 'Status tidak valid']); + } + + try { + DB::table('laporan_bencana') + ->where('id', $id) + ->update(['status' => $status]); + + return response()->json(['success' => true]); + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => 'Gagal mengupdate status']); + } + } + + public function destroy($id) + { + $laporan = DB::table('laporan_bencana')->where('id', $id)->first(); + if ($laporan) { + // Hapus file foto jika ada + if ($laporan->foto) { + $path = public_path('storage/' . $laporan->foto); + if (file_exists($path)) { + unlink($path); + } + } + + DB::table('laporan_bencana')->where('id', $id)->delete(); + return response()->json(['success' => true]); + } + return response()->json(['success' => false]); + } + + private function checkLaporanLimit($userId) + { + // Jika user ID adalah 60, 61, atau 62, tidak ada batasan + if (in_array($userId, [60, 61, 62])) { + return true; + } + + // Hitung jumlah laporan hari ini + $today = now()->startOfDay(); + $count = DB::table('laporan_bencana') + ->where('user_id', $userId) + ->where('created_at', '>=', $today) + ->count(); + + // Batasi 10 laporan per hari + return $count < 10; + } + + private function isWithinLumajang($latitude, $longitude) + { + // Koordinat batas wilayah Lumajang + $minLat = -8.5; // Batas selatan + $maxLat = -7.8; // Batas utara + $minLng = 112.8; // Batas barat + $maxLng = 113.4; // Batas timur + + return ($latitude >= $minLat && $latitude <= $maxLat) && + ($longitude >= $minLng && $longitude <= $maxLng); + } + + public function store(Request $request) + { + try { + // Validasi koordinat + if (!$this->isWithinLumajang($request->latitude, $request->longitude)) { + return response()->json([ + 'success' => false, + 'message' => 'Lokasi yang dipilih berada di luar wilayah Lumajang' + ], 400); + } + + // Cek batasan laporan + if (!$this->checkLaporanLimit(Auth::id())) { + return response()->json([ + 'success' => false, + 'message' => 'Anda telah mencapai batas maksimal laporan hari ini (10 laporan)' + ], 400); + } + + // Validasi input + $request->validate([ + 'lokasi' => 'required|string', + 'latitude' => 'required|numeric', + 'longitude' => 'required|numeric', + 'jenis_bencana' => 'required|in:Banjir,Longsor,Erupsi,Lahar Panas,Lahar Dingin,Gempa,Angin Topan', + 'deskripsi' => 'nullable|string', + 'foto' => 'nullable|image|max:5120', // maksimal 5MB + ]); + + // Upload foto + $fotoPath = null; + if ($request->hasFile('foto')) { + $foto = $request->file('foto'); + $fileName = time() . '_' . $foto->getClientOriginalName(); + $fotoPath = $foto->storeAs('laporan-bencana', $fileName, 'public'); + } + + // Simpan data ke database + $laporan = LaporanBencana::create([ + 'user_id' => Auth::id(), // Simpan ID pengguna yang sedang login + 'lokasi' => $request->lokasi, + 'latitude' => $request->latitude, + 'longitude' => $request->longitude, + 'jenis_bencana' => $request->jenis_bencana, + 'deskripsi' => $request->deskripsi, + 'foto' => $fotoPath, + 'status' => 'menunggu', + ]); + + // Kirim notifikasi ke semua admin + $admins = \App\Models\User::where('tipe_pengguna', 'admin')->orWhere('tipe_pengguna', 'super admin')->get(); + foreach ($admins as $admin) { + $admin->notify(new \App\Notifications\NewLaporanBencanaNotification($laporan)); + } + + broadcast(new LaporanDibuat($laporan)); + + return response()->json([ + 'success' => true, + 'message' => 'Laporan berhasil dikirim', + 'data' => [ + 'id' => $laporan->id, + 'foto_url' => $fotoPath ? asset('storage/' . $fotoPath) : null + ] + ], 201); + } catch (\Exception $e) { + // Hapus foto jika upload gagal + if (isset($fotoPath)) { + Storage::disk('public')->delete($fotoPath); + } + + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengirim laporan', + 'error' => $e->getMessage() + ], 500); + } + } + + public function rekap($format) + { + $tahun = request('tahun'); + $bulan = request('bulan'); + + $query = DB::table('laporan_bencana') + ->join('users', 'laporan_bencana.user_id', '=', 'users.id') + ->select('laporan_bencana.*', 'users.name as pelapor'); + + if ($bulan) { + $query->whereYear('laporan_bencana.created_at', $tahun) + ->whereMonth('laporan_bencana.created_at', $bulan); + } else { + $query->whereYear('laporan_bencana.created_at', $tahun); + } + + $data = $query->get(); + + if ($format === 'pdf') { + $pdf = PDF::loadView('bencana.rekap', [ + 'data' => $data, + 'tahun' => $tahun, + 'bulan' => $bulan + ]); + + return $pdf->download('rekap-bencana-' . ($bulan ? $tahun . '-' . $bulan : $tahun) . '.pdf'); + } else { + return Excel::download(new BencanaExport($data), 'rekap-bencana-' . ($bulan ? $tahun . '-' . $bulan : $tahun) . '.xlsx'); + } + } +} diff --git a/app/Http/Controllers/LaporanJalanController.php b/app/Http/Controllers/LaporanJalanController.php new file mode 100644 index 0000000..947dfd7 --- /dev/null +++ b/app/Http/Controllers/LaporanJalanController.php @@ -0,0 +1,219 @@ +join('users', 'laporan_jalan.user_id', '=', 'users.id') + ->select('laporan_jalan.*', 'users.name as pelapor') + ->orderBy('created_at', 'desc') + ->get(); + + return view('jalan.index', compact('laporanJalan')); + } + + public function getLaporanJalan() + { + $laporanJalan = DB::table('laporan_jalan') + ->select('latitude', 'longitude', 'status', 'jenis_rusak') + ->whereIn('status', ['menunggu', 'dalam_proses']) + ->get(); + + return response()->json($laporanJalan); + } + + public function updateStatus(Request $request, $id) + { + $status = $request->status; + $allowed_status = ['menunggu', 'dalam_proses', 'selesai']; + + if (!in_array($status, $allowed_status)) { + return response()->json(['success' => false, 'message' => 'Status tidak valid']); + } + + try { + DB::table('laporan_jalan') + ->where('id', $id) + ->update(['status' => $status]); + + return response()->json(['success' => true]); + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => 'Gagal mengupdate status']); + } + } + + public function destroy($id) + { + $laporan = DB::table('laporan_jalan')->where('id', $id)->first(); + if ($laporan) { + // Hapus file foto jika ada + if ($laporan->foto) { + $path = public_path('storage/' . $laporan->foto); + if (file_exists($path)) { + unlink($path); + } + } + + DB::table('laporan_jalan')->where('id', $id)->delete(); + return response()->json(['success' => true]); + } + return response()->json(['success' => false]); + } + + private function checkLaporanLimit($userId) + { + // Jika user ID adalah 60, 61, atau 62, tidak ada batasan + if (in_array($userId, [60, 61, 62])) { + return true; + } + + // Hitung jumlah laporan hari ini + $today = now()->startOfDay(); + $count = DB::table('laporan_jalan') + ->where('user_id', $userId) + ->where('created_at', '>=', $today) + ->count(); + + // Batasi 10 laporan per hari + return $count < 10; + } + + private function isWithinLumajang($latitude, $longitude) + { + // Koordinat batas wilayah Lumajang + $minLat = -8.5; // Batas selatan + $maxLat = -7.8; // Batas utara + $minLng = 112.8; // Batas barat + $maxLng = 113.4; // Batas timur + + return ($latitude >= $minLat && $latitude <= $maxLat) && + ($longitude >= $minLng && $longitude <= $maxLng); + } + + public function store(Request $request) + { + try { + // Validasi koordinat + if (!$this->isWithinLumajang($request->latitude, $request->longitude)) { + return response()->json([ + 'success' => false, + 'message' => 'Lokasi yang dipilih berada di luar wilayah Lumajang' + ], 400); + } + + // Cek batasan laporan + if (!$this->checkLaporanLimit(Auth::id())) { + return response()->json([ + 'success' => false, + 'message' => 'Anda telah mencapai batas maksimal laporan hari ini (10 laporan)' + ], 400); + } + + // Validasi input + $request->validate([ + 'lokasi' => 'required|string', + 'latitude' => 'required|numeric', + 'longitude' => 'required|numeric', + 'jenis_rusak' => 'required|in:jalan rusak ringan,jalan rusak berat', + 'deskripsi' => 'nullable|string', + 'foto' => 'nullable|image|max:5120', // maksimal 5MB + ]); + + // Upload foto + $fotoPath = null; + if ($request->hasFile('foto')) { + $foto = $request->file('foto'); + $fileName = time() . '_' . $foto->getClientOriginalName(); + $fotoPath = $foto->storeAs('laporan-jalan', $fileName, 'public'); + } + + // Simpan data ke database + $laporan = LaporanJalan::create([ + 'user_id' => Auth::id(), + 'lokasi' => $request->lokasi, + 'latitude' => $request->latitude, + 'longitude' => $request->longitude, + 'jenis_rusak' => $request->jenis_rusak, + 'deskripsi' => $request->deskripsi, + 'foto' => $fotoPath, + 'status' => 'menunggu', + ]); + + // Kirim notifikasi ke semua admin + $admins = \App\Models\User::where('tipe_pengguna', 'admin')->orWhere('tipe_pengguna', 'super admin')->get(); + foreach ($admins as $admin) { + $admin->notify(new \App\Notifications\NewLaporanJalanNotification($laporan)); + } + + broadcast(new LaporanDibuat($laporan)); + + return response()->json([ + 'success' => true, + 'message' => 'Laporan berhasil dikirim', + 'data' => [ + 'id' => $laporan->id, + 'foto_url' => $fotoPath ? asset('storage/' . $fotoPath) : null + ] + ], 201); + } catch (\Exception $e) { + // Hapus foto jika upload gagal + if (isset($fotoPath)) { + Storage::disk('public')->delete($fotoPath); + } + + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengirim laporan', + 'error' => $e->getMessage() + ], 500); + } + } + + public function rekap($format) + { + $tahun = request('tahun'); + $bulan = request('bulan'); + + $query = DB::table('laporan_jalan') + ->join('users', 'laporan_jalan.user_id', '=', 'users.id') + ->select('laporan_jalan.*', 'users.name as pelapor'); + + if ($bulan) { + $query->whereYear('laporan_jalan.created_at', $tahun) + ->whereMonth('laporan_jalan.created_at', $bulan); + } else { + $query->whereYear('laporan_jalan.created_at', $tahun); + } + + $data = $query->get(); + + if ($format === 'pdf') { + $pdf = PDF::loadView('jalan.rekap', [ + 'data' => $data, + 'tahun' => $tahun, + 'bulan' => $bulan + ]); + + return $pdf->download('rekap-jalan-' . ($bulan ? $tahun . '-' . $bulan : $tahun) . '.pdf'); + } else { + return Excel::download(new JalanExport($data), 'rekap-jalan-' . ($bulan ? $tahun . '-' . $bulan : $tahun) . '.xlsx'); + } + } +} diff --git a/app/Http/Controllers/NotificationController.php b/app/Http/Controllers/NotificationController.php new file mode 100644 index 0000000..bbe51ad --- /dev/null +++ b/app/Http/Controllers/NotificationController.php @@ -0,0 +1,59 @@ +where('notifiable_id', Auth::id()) + ->whereNull('read_at') + ->count(); + + return response()->json(['count' => $count]); + } + + public function deleteRead() + { + DB::table('notifications') + ->where('notifiable_id', Auth::id()) + ->whereNotNull('read_at') + ->delete(); + + return response()->json(['message' => 'Notifikasi yang sudah dibaca berhasil dihapus']); + } + + public function markAsRead($id) + { + $notification = DB::table('notifications')->where('id', $id)->first(); + + if ($notification && $notification->notifiable_id == Auth::id()) { + DB::table('notifications') + ->where('id', $id) + ->update(['read_at' => Carbon::now()]); + + return response()->json(['message' => 'Notifikasi ditandai sebagai sudah dibaca']); + } + + return response()->json(['message' => 'Notifikasi tidak ditemukan'], 404); + } + + public function markAllRead() + { + DB::table('notifications') + ->where('notifiable_id', Auth::id()) + ->whereNull('read_at') + ->update(['read_at' => Carbon::now()]); + + return response()->json(['message' => 'Semua notifikasi ditandai sebagai sudah dibaca']); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/PrivacyPolicyController.php b/app/Http/Controllers/PrivacyPolicyController.php new file mode 100644 index 0000000..154f028 --- /dev/null +++ b/app/Http/Controllers/PrivacyPolicyController.php @@ -0,0 +1,13 @@ +validate([ + 'name' => 'required|string|max:255', + ]); + + $user = JWTAuth::parseToken()->authenticate(); + + $user->update([ + 'name' => $request->name, + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Profil berhasil diperbarui', + 'data' => [ + 'name' => $user->name + ] + ]); + } catch (\Tymon\JWTAuth\Exceptions\TokenExpiredException $e) { + return response()->json([ + 'success' => false, + 'message' => 'Token telah kadaluarsa' + ], 401); + } catch (\Tymon\JWTAuth\Exceptions\TokenInvalidException $e) { + return response()->json([ + 'success' => false, + 'message' => 'Token tidak valid' + ], 401); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal memperbarui profil', + 'error' => $e->getMessage() + ], 500); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/ResetPasswordController.php b/app/Http/Controllers/ResetPasswordController.php new file mode 100644 index 0000000..20228aa --- /dev/null +++ b/app/Http/Controllers/ResetPasswordController.php @@ -0,0 +1,79 @@ +where('reset_password_expires_at', '>', now()) + ->first(); + + if (!$user) { + \Illuminate\Support\Facades\Log::error('Token tidak ditemukan atau kadaluarsa: ' . $token); + return view('reset_password.failed', [ + 'message' => 'Link reset password tidak valid atau sudah kadaluarsa' + ]); + } + + return view('reset_password.form', [ + 'token' => $token, + 'email' => $user->email + ]); + + } catch (\Exception $e) { + \Illuminate\Support\Facades\Log::error('Reset password error: ' . $e->getMessage()); + return view('reset_password.failed', [ + 'message' => 'Terjadi kesalahan saat memproses reset password' + ]); + } + } + + public function reset(Request $request) + { + $validator = Validator::make($request->all(), [ + 'token' => 'required', + 'password' => 'required|min:6|confirmed', + ]); + + if ($validator->fails()) { + return back()->withErrors($validator)->withInput(); + } + + try { + $user = User::where('reset_password_token', $request->token) + ->where('reset_password_expires_at', '>', now()) + ->first(); + + if (!$user) { + return view('reset_password.failed', [ + 'message' => 'Link reset password tidak valid atau sudah kadaluarsa' + ]); + } + + $user->password = Hash::make($request->password); + $user->reset_password_token = null; + $user->reset_password_expires_at = null; + $user->save(); + + return view('reset_password.success', [ + 'message' => 'Password berhasil diperbarui. Silakan login dengan password baru Anda.' + ]); + + } catch (\Exception $e) { + \Illuminate\Support\Facades\Log::error('Reset password error: ' . $e->getMessage()); + return view('reset_password.failed', [ + 'message' => 'Terjadi kesalahan saat memproses reset password' + ]); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/ResetPasswordRedirectController.php b/app/Http/Controllers/ResetPasswordRedirectController.php new file mode 100644 index 0000000..ff6ac30 --- /dev/null +++ b/app/Http/Controllers/ResetPasswordRedirectController.php @@ -0,0 +1,23 @@ + $appUrl, + 'token' => $token, + 'email' => $email + ]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php new file mode 100644 index 0000000..4d261b5 --- /dev/null +++ b/app/Http/Controllers/UserController.php @@ -0,0 +1,129 @@ +validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:admins,email,' . $user->id, + 'alamat' => 'required|string', + 'telepon' => 'required|string', + ]); + + Admin::where('id', $user->id)->update([ + 'name' => $request->name, + 'email' => $request->email, + 'alamat' => $request->alamat, + 'telepon' => $request->telepon, + ]); + + return back()->with('success', 'Profile berhasil diperbarui.'); + } + + public function updatePassword(Request $request) + { + $request->validate([ + 'current_password' => 'required|string', + 'password' => 'required|string|min:8|confirmed', + ]); + + $user = Auth::user(); + + if (!Hash::check($request->current_password, $user->password)) { + return back()->withErrors(['current_password' => 'Password saat ini tidak sesuai.']); + } + + Admin::where('id', $user->id)->update([ + 'password' => Hash::make($request->password) + ]); + + return back()->with('success', 'Password berhasil diperbarui.'); + } + + // Admin management methods + public function index() + { + $users = User::where('tipe_pengguna', 'user')->get(); + return view('users.index', compact('users')); + } + + public function create() + { + return view('users.create'); + } + + public function store(Request $request) + { + $request->validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:admins', + 'password' => 'required|string|min:8|confirmed', + 'alamat' => 'required|string', + 'telepon' => 'required|string', + ]); + + Admin::create([ + 'name' => $request->name, + 'email' => $request->email, + 'password' => Hash::make($request->password), + 'alamat' => $request->alamat, + 'telepon' => $request->telepon, + 'tipe_pengguna' => 'user', + ]); + + return redirect()->route('users.index') + ->with('success', 'User berhasil ditambahkan.'); + } + + public function edit(Admin $user) + { + return view('users.edit', compact('user')); + } + + public function update(Request $request, Admin $user) + { + $request->validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:admins,email,' . $user->id, + 'alamat' => 'required|string', + 'telepon' => 'required|string', + ]); + + Admin::where('id', $user->id)->update([ + 'name' => $request->name, + 'email' => $request->email, + 'alamat' => $request->alamat, + 'telepon' => $request->telepon, + ]); + + return redirect()->route('users.index') + ->with('success', 'User berhasil diperbarui.'); + } + + public function destroy($id) + { + $user = User::find($id); + if ($user) { + $user->delete(); + return response()->json(['success' => true]); + } + return response()->json(['success' => false]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/VerificationController.php b/app/Http/Controllers/VerificationController.php new file mode 100644 index 0000000..d677686 --- /dev/null +++ b/app/Http/Controllers/VerificationController.php @@ -0,0 +1,43 @@ +first(); + + if (!$user) { + \Illuminate\Support\Facades\Log::error('Token tidak ditemukan: ' . $token); + return view('verification.failed', [ + 'message' => 'Link verifikasi tidak valid atau sudah kadaluarsa' + ]); + } + + $user->is_verified = true; + $user->email_verified_at = now(); + $user->verification_token = null; + $user->save(); + + \Illuminate\Support\Facades\Log::info('Email berhasil diverifikasi: ' . $user->email); + + return view('verification.success', [ + 'name' => $user->name, + 'message' => 'Email berhasil diverifikasi' + ]); + + } catch (\Exception $e) { + \Illuminate\Support\Facades\Log::error('Verification error: ' . $e->getMessage()); + return view('verification.failed', [ + 'message' => 'Terjadi kesalahan saat verifikasi' + ]); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/WeatherController.php b/app/Http/Controllers/WeatherController.php new file mode 100644 index 0000000..9786248 --- /dev/null +++ b/app/Http/Controllers/WeatherController.php @@ -0,0 +1,96 @@ +apiKey = config('services.weatherapi.key'); + } + + public function getWeather(Request $request) + { + try { + $lat = $request->query('lat'); + $lon = $request->query('lon'); + + Log::info('Weather request params:', [ + 'lat' => $lat, + 'lon' => $lon + ]); + + // Get weather data from WeatherAPI.com + $response = Http::get("http://api.weatherapi.com/v1/forecast.json", [ + 'key' => $this->apiKey, + 'q' => "$lat,$lon", + 'days' => 1, + 'aqi' => 'no', + 'lang' => 'id' + ]); + + Log::info('WeatherAPI response:', [ + 'status' => $response->status(), + 'body' => $response->body() + ]); + + if (!$response->successful()) { + throw new \Exception('Failed to fetch weather data: ' . $response->body()); + } + + $weatherData = $response->json(); + $currentHour = now()->format('H'); + + // Get current weather from hourly forecast + $currentWeather = collect($weatherData['forecast']['forecastday'][0]['hour']) + ->first(function($hour) use ($currentHour) { + return date('H', strtotime($hour['time'])) == $currentHour; + }); + + return response()->json([ + 'success' => true, + 'location' => [ + 'name' => $weatherData['location']['name'], + 'state' => $weatherData['location']['region'], + 'country' => $weatherData['location']['country'], + 'lat' => $weatherData['location']['lat'], + 'lon' => $weatherData['location']['lon'] + ], + 'current_weather' => [ + 'datetime' => $currentWeather['time'], + 'temp' => round($currentWeather['temp_c']), + 'humidity' => $currentWeather['humidity'], + 'description' => $currentWeather['condition']['text'], + 'icon' => $currentWeather['condition']['icon'], + ], + 'weather' => array_map(function($hour) { + return [ + 'datetime' => $hour['time'], + 'temp' => round($hour['temp_c']), + 'humidity' => $hour['humidity'], + 'description' => $hour['condition']['text'], + 'icon' => $hour['condition']['icon'], + ]; + }, $weatherData['forecast']['forecastday'][0]['hour']) + ]); + } catch (\Exception $e) { + Log::error('Weather API error:', [ + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil data cuaca', + 'error' => $e->getMessage() + ], 500); + } + } +} \ No newline at end of file diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php new file mode 100644 index 0000000..db2f165 --- /dev/null +++ b/app/Http/Kernel.php @@ -0,0 +1,75 @@ + + */ + protected $middleware = [ + // \App\Http\Middleware\TrustHosts::class, + \App\Http\Middleware\TrustProxies::class, + \Illuminate\Http\Middleware\HandleCors::class, + \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, + \App\Http\Middleware\TrimStrings::class, + \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, + ]; + + /** + * The application's route middleware groups. + * + * @var arrayAplikasi Jaga Jalan menghormati dan melindungi privasi pengguna. Kebijakan privasi ini menjelaskan jenis data yang dikumpulkan, bagaimana data digunakan, dan bagaimana data disimpan.
+ +Aplikasi ini mengumpulkan beberapa jenis data dari pengguna, termasuk:
+Data yang dikumpulkan akan digunakan untuk:
+Data pengguna tidak dibagikan kepada pihak ketiga luar. Namun, data seperti lokasi dan email dapat diakses oleh admin aplikasi hanya untuk keperluan validasi dan manajemen laporan.
+Saat ini, pengguna belum dapat menghapus akun atau data secara mandiri melalui aplikasi. Namun, pengguna dapat menghubungi pihak pengelola aplikasi jika ingin menghapus data mereka.
+Kami berkomitmen menjaga data pengguna dengan menggunakan sistem keamanan yang sesuai standar.
+Kebijakan ini dapat diperbarui sewaktu-waktu. Perubahan akan diinformasikan melalui aplikasi atau media terkait lainnya.
+No | +Nama | +Alamat | +Instansi | +Jabatan | +Tipe Pengguna | + @if($currentAdmin->tipe_pengguna === 'super admin') +Aksi | + @endif +|
---|---|---|---|---|---|---|---|
{{ $index + 1 }} | ++ {{ $admin->name }} + @if($admin->id === $currentAdmin->id) + Anda + @endif + | +{{ $admin->email }} | +{{ $admin->alamat }} | +{{ $admin->instansi }} | ++ + {{ $admin->jabatan }} + + | ++ + {{ ucfirst($admin->tipe_pengguna) }} + + | + @if($currentAdmin->tipe_pengguna === 'super admin') ++ @if($admin->id !== $currentAdmin->id) + + @else + + @endif + | + @endif +
No | +Pelapor | +Jenis Bencana | +Lokasi | +Status | +Tanggal | +Aksi | +
---|---|---|---|---|---|---|
{{ $index + 1 }} | +{{ $laporan->pelapor }} | ++ {{ ucfirst($laporan->jenis_bencana) }} + | ++ {{ $laporan->lokasi }} + + | +
+
+
+
+
+ |
+ {{ \Carbon\Carbon::parse($laporan->created_at)->format('d M Y H:i') }} | ++ + + | +
Periode: {{ $bulan ? \Carbon\Carbon::create()->month($bulan)->format('F') . ' ' . $tahun : $tahun }}
+No | +Pelapor | +Jenis Bencana | +Lokasi | +Status | +Tanggal | +Deskripsi | +
---|---|---|---|---|---|---|
{{ $index + 1 }} | +{{ $item->pelapor }} | +{{ ucfirst($item->jenis_bencana) }} | +{{ $item->lokasi }} | +{{ str_replace('_', ' ', ucfirst($item->status)) }} | +{{ \Carbon\Carbon::parse($item->created_at)->format('d M Y H:i') }} | +{{ $item->deskripsi }} | +
Halo {{ $user->name }},
+ +Kami menerima permintaan untuk reset password akun Anda. Silakan klik tombol di bawah ini untuk membuat password baru:
+ +
+ Jika tombol di atas tidak berfungsi, coba salin link berikut ke browser Anda:
+
+ {{ $resetUrl }}
+
Jika Anda tidak meminta reset password, abaikan email ini dan password Anda tidak akan berubah.
+ +Link ini akan kadaluarsa dalam 24 jam.
+ +Terima kasih,
Tim Jaga Jalan
Terima kasih telah mendaftar. Silakan klik link di bawah ini untuk memverifikasi email Anda:
+ Verifikasi Email +Link ini akan kadaluarsa dalam 24 jam.
+ + \ No newline at end of file diff --git a/resources/views/jalan/index.blade.php b/resources/views/jalan/index.blade.php new file mode 100644 index 0000000..810f4ae --- /dev/null +++ b/resources/views/jalan/index.blade.php @@ -0,0 +1,689 @@ +@extends('layouts.app') + +@section('title', 'Daftar Laporan Jalan') +@section('page-title', 'Daftar Laporan Jalan') + +@section('content') +No | +Pelapor | +Jenis Kerusakan | +Lokasi | +Status | +Tanggal | +Aksi | +
---|---|---|---|---|---|---|
{{ $index + 1 }} | +{{ $laporan->pelapor }} | ++ {{ ucfirst($laporan->jenis_rusak) }} + | ++ {{ $laporan->lokasi }} + + | +
+
+
+
+
+ |
+ {{ \Carbon\Carbon::parse($laporan->created_at)->format('d M Y H:i') }} | ++ + + | +
Periode: {{ $bulan ? \Carbon\Carbon::create()->month($bulan)->format('F') . ' ' . $tahun : $tahun }}
+No | +Pelapor | +Jenis Kerusakan | +Lokasi | +Status | +Tanggal | +Deskripsi | +
---|---|---|---|---|---|---|
{{ $index + 1 }} | +{{ $item->pelapor }} | +{{ ucfirst($item->jenis_rusak) }} | +{{ $item->lokasi }} | +{{ str_replace('_', ' ', ucfirst($item->status)) }} | +{{ \Carbon\Carbon::parse($item->created_at)->format('d M Y H:i') }} | +{{ $item->deskripsi }} | +
Anda akan otomatis diarahkan ke aplikasi Jaga Jalan untuk reset password...
+ + +No | +Nama | +Alamat | +Aksi | +|
---|---|---|---|---|
{{ $index + 1 }} | +{{ $user->name }} | +{{ $user->email }} | +{{ $user->alamat }} | ++ + | +
Selamat {{ $name }}, email Anda telah terverifikasi. Sekarang Anda dapat menggunakan aplikasi Jaga Jalan.
+ Buka Aplikasi + ++ Laravel has wonderful documentation covering every aspect of the framework. Whether you are a newcomer or have prior experience with Laravel, we recommend reading our documentation from beginning to end. +
++ Laracasts offers thousands of video tutorials on Laravel, PHP, and JavaScript development. Check them out, see for yourself, and massively level up your development skills in the process. +
++ Laravel News is a community driven portal and newsletter aggregating all of the latest and most important news in the Laravel ecosystem, including new package releases and tutorials. +
+