commit e557826642f9c04148b5326235550ec6333a3733 Author: miftaoktaaa Date: Wed Jun 17 16:08:34 2026 +0700 first commit diff --git a/.env b/.env new file mode 100644 index 0000000..9455df7 --- /dev/null +++ b/.env @@ -0,0 +1,60 @@ +APP_NAME=Laravel +APP_ENV=production +APP_KEY=base64:wIhXfVX/DNNDUeNSEpSnl7PeOjzWdrgvkChBzmEiu4M= +APP_DEBUG=true +APP_URL=https://ta.myhost.id/E31230906 +APP_TIMEZONE=Asia/Jakarta + +LOG_CHANNEL=stack +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=localhost +DB_PORT=3306 +DB_DATABASE=tamyhost_E31230906 +DB_USERNAME=tamyhost_E31230906 +DB_PASSWORD=MIF_E31230906 + +BROADCAST_DRIVER=log +CACHE_DRIVER=file +FILESYSTEM_DISK=local +QUEUE_CONNECTION=sync +SESSION_DRIVER=cookie +SESSION_LIFETIME=120 + +MEMCACHED_HOST=127.0.0.1 + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=smtp +MAIL_HOST=live.smtp.mailtrap.io +MAIL_PORT=587 +MAIL_USERNAME=api +MAIL_PASSWORD=28971c622d8cfb592be491a9e4306bdb +MAIL_ENCRYPTION=tls +MAIL_FROM_ADDRESS=hello@demomailtrap.com +MAIL_FROM_NAME="SIPDAM - Tirta Sanjiwani" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +PUSHER_APP_ID= +PUSHER_APP_KEY= +PUSHER_APP_SECRET= +PUSHER_HOST= +PUSHER_PORT=443 +PUSHER_SCHEME=https +PUSHER_APP_CLUSTER=mt1 + +VITE_APP_NAME="${APP_NAME}" +VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" +VITE_PUSHER_HOST="${PUSHER_HOST}" +VITE_PUSHER_PORT="${PUSHER_PORT}" +VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" +VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" \ No newline at end of file diff --git a/samooapk/.editorconfig b/samooapk/.editorconfig new file mode 100644 index 0000000..8f0de65 --- /dev/null +++ b/samooapk/.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/samooapk/.env.example b/samooapk/.env.example new file mode 100644 index 0000000..ea0665b --- /dev/null +++ b/samooapk/.env.example @@ -0,0 +1,59 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +LOG_CHANNEL=stack +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=laravel +DB_USERNAME=root +DB_PASSWORD= + +BROADCAST_DRIVER=log +CACHE_DRIVER=file +FILESYSTEM_DISK=local +QUEUE_CONNECTION=sync +SESSION_DRIVER=file +SESSION_LIFETIME=120 + +MEMCACHED_HOST=127.0.0.1 + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=smtp +MAIL_HOST=mailpit +MAIL_PORT=1025 +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 + +PUSHER_APP_ID= +PUSHER_APP_KEY= +PUSHER_APP_SECRET= +PUSHER_HOST= +PUSHER_PORT=443 +PUSHER_SCHEME=https +PUSHER_APP_CLUSTER=mt1 + +VITE_APP_NAME="${APP_NAME}" +VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" +VITE_PUSHER_HOST="${PUSHER_HOST}" +VITE_PUSHER_PORT="${PUSHER_PORT}" +VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" +VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" diff --git a/samooapk/.gitattributes b/samooapk/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/samooapk/.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/samooapk/.gitignore b/samooapk/.gitignore new file mode 100644 index 0000000..7fe978f --- /dev/null +++ b/samooapk/.gitignore @@ -0,0 +1,19 @@ +/.phpunit.cache +/node_modules +/public/build +/public/hot +/public/storage +/storage/*.key +/vendor +.env +.env.backup +.env.production +.phpunit.result.cache +Homestead.json +Homestead.yaml +auth.json +npm-debug.log +yarn-error.log +/.fleet +/.idea +/.vscode diff --git a/samooapk/README.md b/samooapk/README.md new file mode 100644 index 0000000..1a4c26b --- /dev/null +++ b/samooapk/README.md @@ -0,0 +1,66 @@ +

Laravel Logo

+ +

+Build Status +Total Downloads +Latest Stable Version +License +

+ +## 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/samooapk/app/Console/Commands/MigrateOldCatatanData.php b/samooapk/app/Console/Commands/MigrateOldCatatanData.php new file mode 100644 index 0000000..21e6b42 --- /dev/null +++ b/samooapk/app/Console/Commands/MigrateOldCatatanData.php @@ -0,0 +1,67 @@ +whereNotNull('catatan_admin') + ->get(); + + $this->info("Ditemukan {$penugasans->count()} data lama yang perlu dimigrasi..."); + $updated = 0; + + foreach ($penugasans as $p) { + $catatan = $p->catatan_admin; + $nama = null; + $alamat = null; + $noSamb = null; + + // ── Cari Nama ────────────────────────────────── + // Pola: "Nama : ...", "Nama DR ...", "Nama: ..." + if (preg_match('/Nama\s*[:\-]?\s*(.+?)(?=Alamat|Pekerjaan|No\.?|$)/is', $catatan, $m)) { + $nama = trim($m[1]); + } + + // ── Cari Alamat ───────────────────────────────── + // Pola: "Alamat: ...", "Alamat BR. ..." + if (preg_match('/Alamat\s*[:\-]?\s*(.+?)(?=Pekerjaan|No\.?|Nama|$)/is', $catatan, $m)) { + $alamat = trim($m[1]); + } + + // ── Cari No Sambungan ─────────────────────────── + // Pola: "no sambungan 0032", "No sambungannya 0008", "no samb 0032" + if (preg_match('/no\.?\s*samb(?:ungan(?:nya)?)?\s*[:\-]?\s*([0-9]+)/is', $catatan, $m)) { + $noSamb = trim($m[1]); + } + + // Bersihkan trailing punct dari hasil parsing + $nama = $nama ? rtrim($nama, ' ,;.') : null; + $alamat = $alamat ? rtrim($alamat, ' ,;.') : null; + + // Hanya update kalau minimal ada salah satu yang berhasil di-parse + if ($nama || $alamat || $noSamb) { + $p->update([ + 'nama_pelanggan' => $nama, + 'alamat_lokasi' => $alamat, + 'no_sambungan' => $noSamb, + ]); + $updated++; + $this->line(" ✅ ID #{$p->id_penugasan} → Nama: $nama | Alamat: $alamat | No: $noSamb"); + } else { + $this->line(" ⚠️ ID #{$p->id_penugasan} → Format tidak dikenali: \"{$catatan}\""); + } + } + + $this->info("\n✅ Selesai! $updated dari {$penugasans->count()} data berhasil dimigrasi."); + return 0; + } +} diff --git a/samooapk/app/Console/Kernel.php b/samooapk/app/Console/Kernel.php new file mode 100644 index 0000000..e6b9960 --- /dev/null +++ b/samooapk/app/Console/Kernel.php @@ -0,0 +1,27 @@ +command('inspire')->hourly(); + } + + /** + * Register the commands for the application. + */ + protected function commands(): void + { + $this->load(__DIR__.'/Commands'); + + require base_path('routes/console.php'); + } +} diff --git a/samooapk/app/Exceptions/Handler.php b/samooapk/app/Exceptions/Handler.php new file mode 100644 index 0000000..56af264 --- /dev/null +++ b/samooapk/app/Exceptions/Handler.php @@ -0,0 +1,30 @@ + + */ + protected $dontFlash = [ + 'current_password', + 'password', + 'password_confirmation', + ]; + + /** + * Register the exception handling callbacks for the application. + */ + public function register(): void + { + $this->reportable(function (Throwable $e) { + // + }); + } +} diff --git a/samooapk/app/Http/Controllers/AbsensiController.php b/samooapk/app/Http/Controllers/AbsensiController.php new file mode 100644 index 0000000..97cebbb --- /dev/null +++ b/samooapk/app/Http/Controllers/AbsensiController.php @@ -0,0 +1,218 @@ +get(); + + // Query dasar untuk data utama + $query = Absensi::with('teknisi'); + + // Filter Rentang Tanggal + if ($request->filled('start_date') && $request->filled('end_date')) { + $query->whereBetween('tanggal', [$request->start_date, $request->end_date]); + } elseif ($request->filled('start_date')) { + $query->whereDate('tanggal', '>=', $request->start_date); + } elseif ($request->filled('end_date')) { + $query->whereDate('tanggal', '<=', $request->end_date); + } + + // Filter Teknisi + if ($request->filled('teknisi')) { + $query->where('id_teknisi', $request->teknisi); + } + + // Hitung statistik berdasarkan filter di atas (sebelum filter status) + $counts = [ + 'total' => (clone $query)->count(), + 'hadir' => (clone $query)->where('status', 'hadir')->count(), + 'izin' => (clone $query)->whereIn('status', ['izin', 'sakit'])->count(), + ]; + + // Apply filter status untuk tabel + if ($request->query('status') === 'izin') { + $query->whereIn('status', ['izin', 'sakit']); + } else { + $query->filterByStatus($request->query('status')); + } + + $absensis = $query->orderBy('tanggal', 'desc') + ->orderBy('jam_masuk', 'desc') + ->paginate(15); + + return view('Admin.KelolaTeknisi.Absensi', compact('absensis', 'teknisis', 'counts')); + } + + /** + * Menampilkan detail data absensi spesifik. + */ + public function show($id) + { + $absensi = Absensi::with('teknisi')->where('id_absensi', $id)->first(); + + if (!$absensi) { + return response()->json([ + 'error' => 'Data absensi tidak ditemukan.' + ], 404); + } + + // Generate URL yang benar untuk hosting + $fotoMasukUrl = null; + $fotoKeluarUrl = null; + + if ($absensi->foto_absen_masuk) { + $fotoMasukUrl = 'https://ta.myhost.id/E31230906/laravel/public/storage/' . $absensi->foto_absen_masuk; +} + +if ($absensi->foto_absen_keluar) { + $fotoKeluarUrl = 'https://ta.myhost.id/E31230906/laravel/public/storage/' . $absensi->foto_absen_keluar; +} + $data = [ + 'id' => $absensi->id_absensi, + 'tanggal' => $absensi->tanggal instanceof \Carbon\Carbon + ? $absensi->tanggal->format('d/m/Y') + : Carbon::parse($absensi->tanggal)->format('d/m/Y'), + 'tanggal_full' => $absensi->tanggal instanceof \Carbon\Carbon + ? $absensi->tanggal->isoFormat('dddd, D MMMM YYYY') + : Carbon::parse($absensi->tanggal)->isoFormat('dddd, D MMMM YYYY'), + 'jam_masuk' => $absensi->jam_masuk + ? Carbon::parse($absensi->jam_masuk)->format('H:i') + : '-', + 'jam_keluar' => $absensi->jam_keluar + ? Carbon::parse($absensi->jam_keluar)->format('H:i') + : '-', + 'durasi_kerja' => $absensi->durasi_kerja_formatted ?? '00:00', + 'status' => $absensi->status, + 'status_formatted' => ucfirst($absensi->status), + 'status_badge_class' => $absensi->status_badge_class ?? 'badge-secondary', + 'is_terlambat' => $absensi->is_terlambat ?? false, + 'keterangan' => $absensi->keterangan ?? '-', + 'teknisi' => [ + 'id' => $absensi->teknisi ? $absensi->teknisi->id_teknisi : null, + 'nama' => $absensi->teknisi ? $absensi->teknisi->nama : 'Teknisi tidak ditemukan', + 'email' => ($absensi->teknisi && $absensi->teknisi->email) + ? $absensi->teknisi->email + : 'Email tidak tersedia', + ], + 'foto_masuk_url' => $fotoMasukUrl, + 'foto_keluar_url' => $fotoKeluarUrl, + 'kategori_kerja' => $absensi->kategori_kerja, + 'latitude' => $absensi->latitude ?? '0', + 'longitude' => $absensi->longitude ?? '0', + ]; + + return response()->json([ + 'success' => true, + 'data' => $data + ]); + } + + /** + * Mendapatkan statistik absensi + */ + public function getStatistik(Request $request) + { + $id_teknisi = $request->id_teknisi; + $start = $request->start_date; + $end = $request->end_date; + + $data = \DB::table('absensis') + ->whereBetween('tanggal', [$start . ' 00:00:00', $end . ' 23:59:59']) + ->get(); + + $stats = ['hadir' => 0, 'terlambat' => 0, 'izin' => 0, 'sakit' => 0]; + $trendGroups = []; + $rankGroups = []; + $weekly = ['Sen'=>0, 'Sel'=>0, 'Rab'=>0, 'Kam'=>0, 'Jum'=>0]; + $dayMap = [1=>'Min', 2=>'Sen', 3=>'Sel', 4=>'Rab', 5=>'Kam', 6=>'Jum', 7=>'Sab']; + + foreach ($data as $d) { + $status = strtolower($d->status); + $tgl = \Carbon\Carbon::parse($d->tanggal); + + if (!$id_teknisi || $d->id_teknisi == $id_teknisi) { + if (isset($stats[$status])) $stats[$status]++; + $jam = $d->jam_masuk ? \Carbon\Carbon::parse($d->jam_masuk)->format('H:i') : ''; + if ($jam && ($jam < '07:00' || $jam > '18:00')) $stats['terlambat']++; + } + + $week = 'Minggu ' . ceil($tgl->day / 7); + if (!isset($trendGroups[$week])) { + $trendGroups[$week] = ['label' => $week, 'hadir' => 0, 'urgent' => 0]; + } + if ($status == 'hadir') $trendGroups[$week]['hadir']++; + + if (!isset($rankGroups[$d->id_teknisi])) { + $tek = \DB::table('teknisis')->where('id_teknisi', $d->id_teknisi)->first(); + $rankGroups[$d->id_teknisi] = [ + 'name' => $tek ? $tek->nama : 'Unknown', + 'init' => $tek ? strtoupper(substr($tek->nama, 0, 2)) : '??', + 'total' => 0, 'hadir' => 0 + ]; + } + $rankGroups[$d->id_teknisi]['total']++; + if ($status == 'hadir') $rankGroups[$d->id_teknisi]['hadir']++; + + $dayName = $dayMap[$tgl->dayOfWeek + 1] ?? ''; + if (isset($weekly[$dayName]) && $status == 'hadir') $weekly[$dayName]++; + } + + $rankings = collect($rankGroups)->map(function($r) { + $r['pct'] = $r['total'] > 0 ? round(($r['hadir'] / $r['total']) * 100) : 0; + return $r; + })->sortByDesc('pct')->values()->take(5); + + ksort($trendGroups); + $trend = array_values($trendGroups); + + return response()->json([ + 'success' => true, + 'data' => array_merge($stats, [ + 'trend' => $trend, + 'rankings' => $rankings, + 'weekly' => $weekly + ]) + ]); + } + + /** + * Memperbarui data absensi (digunakan oleh Admin). + */ + public function update(Request $request, $id) + { + $request->validate([ + 'jam_masuk' => 'nullable', + 'jam_keluar' => 'nullable', + 'status' => 'required|in:hadir,izin,sakit', + 'keterangan' => 'nullable|string' + ]); + + $absensi = Absensi::findOrFail($id); + $tgl = \Carbon\Carbon::parse($absensi->tanggal)->format('Y-m-d'); + + $absensi->jam_masuk = $request->jam_masuk ? $tgl . ' ' . $request->jam_masuk : null; + $absensi->jam_keluar = $request->jam_keluar ? $tgl . ' ' . $request->jam_keluar : null; + $absensi->status = $request->status; + $absensi->keterangan = $request->keterangan; + + $absensi->save(); + + return response()->json([ + 'success' => true, + 'message' => 'Data absensi berhasil diperbarui.' + ]); + } +} \ No newline at end of file diff --git a/samooapk/app/Http/Controllers/AkunTeknisiController.php b/samooapk/app/Http/Controllers/AkunTeknisiController.php new file mode 100644 index 0000000..0c68fe4 --- /dev/null +++ b/samooapk/app/Http/Controllers/AkunTeknisiController.php @@ -0,0 +1,408 @@ +get(); + $teknisis = Teknisi::whereNotIn('id_teknisi', + AkunTeknisi::pluck('id_teknisi'))->get(); + + return view('Admin.KelolaTeknisi.AkunTeknisi', compact('akunTeknisis', 'teknisis')); + } + + /** + * Tampilkan form untuk membuat akun teknisi baru. + */ + public function create() + { + $teknisi = Teknisi::all(); + return view('Admin.KelolaTeknisi.create-akun', compact('teknisi')); + } + + /** + * Simpan akun teknisi baru ke database. + */ + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'id_teknisi' => 'required|exists:teknisis,id_teknisi|unique:akun_teknisis,id_teknisi', + 'username' => 'required|string|max:255|unique:akun_teknisis,username', + 'password' => 'required|string|min:6', + 'status' => 'required|in:aktif,tidak_aktif', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'errors' => $validator->errors() + ], 422); + } + + try { + AkunTeknisi::create([ + 'id_teknisi' => $request->id_teknisi, + 'username' => $request->username, + 'password' => Hash::make($request->password), + 'password_plain' => $request->password, + 'status' => $request->status, + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Akun teknisi berhasil dibuat!' + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal membuat akun teknisi: ' . $e->getMessage() + ], 500); + } + } + + /** + * Tampilkan detail akun teknisi tertentu. + */ + public function show($id) + { + try { + $akunTeknisi = AkunTeknisi::with('teknisi')->findOrFail($id); + return response()->json($akunTeknisi); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Akun teknisi tidak ditemukan' + ], 404); + } + } + + /** + * Tampilkan form untuk mengedit akun teknisi. + */ + public function edit($id) + { + try { + $akunTeknisi = AkunTeknisi::with('teknisi')->findOrFail($id); + return response()->json($akunTeknisi); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Akun teknisi tidak ditemukan' + ], 404); + } + } + + /** + * Update akun teknisi di database. + */ + public function update(Request $request, $id) + { + try { + $akunTeknisi = AkunTeknisi::findOrFail($id); + + $validator = Validator::make($request->all(), [ + 'id_teknisi' => 'required|exists:teknisis,id_teknisi|unique:akun_teknisis,id_teknisi,' . $id . ',id_akun_teknisi', + 'username' => 'required|string|max:255|unique:akun_teknisis,username,' . $id . ',id_akun_teknisi', + 'password' => 'nullable|string|min:6', + 'status' => 'required|in:aktif,tidak_aktif', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'errors' => $validator->errors() + ], 422); + } + + $updateData = [ + 'id_teknisi' => $request->id_teknisi, + 'username' => $request->username, + 'status' => $request->status, + ]; + + // Hanya update password jika diisi + if ($request->filled('password')) { + $updateData['password'] = Hash::make($request->password); + $updateData['password_plain'] = $request->password; + } + + $akunTeknisi->update($updateData); + + return response()->json([ + 'success' => true, + 'message' => 'Akun teknisi berhasil diupdate!' + ]); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal update akun teknisi: ' . $e->getMessage() + ], 500); + } + } + + /** + * Hapus akun teknisi dari database. + */ + public function destroy($id) + { + try { + $akunTeknisi = AkunTeknisi::findOrFail($id); + $akunTeknisi->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Akun teknisi berhasil dihapus!' + ]); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal hapus akun teknisi: ' . $e->getMessage() + ], 500); + } + } + + /** + * Login untuk teknisi (Mobile App). + */ + public function login(Request $request) + { + $validator = Validator::make($request->all(), [ + 'username' => 'required|string', + 'password' => 'required|string', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed', + 'errors' => $validator->errors() + ], 422); + } + + try { + // Cari akun teknisi + $akun = AkunTeknisi::where('username', $request->username) + ->where('status', 'aktif') + ->with('teknisi') + ->first(); + + // Cek kredensial + if (!$akun || !Hash::check($request->password, $akun->password)) { + return response()->json([ + 'success' => false, + 'message' => 'Username atau password salah' + ], 401); + } + + // Generate JWT token + $token = auth('api')->login($akun); + + if (!$token) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal membuat token' + ], 500); + } + + return response()->json([ + 'success' => true, + 'message' => 'Login berhasil', + 'access_token' => $token, + 'token_type' => 'bearer', + 'expires_in' => auth('api')->factory()->getTTL() * 60, + 'user' => [ + 'id_akun_teknisi' => $akun->id_akun_teknisi, + 'username' => $akun->username, + 'status' => $akun->status, + 'teknisi' => $akun->teknisi + ] + ]); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Terjadi kesalahan saat login: ' . $e->getMessage() + ], 500); + } + } + + /** + * Logout teknisi. + */ + public function logout() + { + try { + auth('api')->logout(); + + return response()->json([ + 'success' => true, + 'message' => 'Logout berhasil' + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Terjadi kesalahan saat logout: ' . $e->getMessage() + ], 500); + } + } + + /** + * Get profile teknisi yang sedang login. + */ + public function me() + { + try { + $akun = auth('api')->user(); + + if (!$akun) { + return response()->json([ + 'success' => false, + 'message' => 'User tidak ditemukan' + ], 404); + } + + // Load relasi teknisi + $akun->load('teknisi'); + + return response()->json([ + 'success' => true, + 'message' => 'Data berhasil diambil', + 'data' => [ + 'id_akun_teknisi' => $akun->id_akun_teknisi, + 'username' => $akun->username, + 'status' => $akun->status, + 'teknisi' => $akun->teknisi + ] + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Terjadi kesalahan: ' . $e->getMessage() + ], 500); + } + } + + /** + * Refresh JWT token. + */ + public function refresh() + { + try { + $newToken = auth('api')->refresh(); + + return response()->json([ + 'success' => true, + 'message' => 'Token berhasil di-refresh', + 'access_token' => $newToken, + 'token_type' => 'bearer', + 'expires_in' => auth('api')->factory()->getTTL() * 60 + ]); + } catch (JWTException $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal refresh token: ' . $e->getMessage() + ], 500); + } + } + + /** + * Change password teknisi. + */ + public function changePassword(Request $request) + { + $validator = Validator::make($request->all(), [ + 'password_lama' => 'required|string', + 'password_baru' => 'required|string|min:6|confirmed', + ], [ + 'password_baru.confirmed' => 'Konfirmasi password tidak sesuai', + 'password_baru.min' => 'Password baru minimal 6 karakter', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validasi gagal', + 'errors' => $validator->errors() + ], 422); + } + + try { + $akun = auth('api')->user(); + + // Cek password lama + if (!Hash::check($request->password_lama, $akun->password)) { + return response()->json([ + 'success' => false, + 'message' => 'Password lama tidak sesuai' + ], 401); + } + + // Update password + $akun->update([ + 'password' => Hash::make($request->password_baru) + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Password berhasil diubah' + ]); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengubah password: ' . $e->getMessage() + ], 500); + } + } + + /** + * Update status akun teknisi. + */ + public function updateStatus(Request $request, $id) + { + try { + $akunTeknisi = AkunTeknisi::findOrFail($id); + + $validator = Validator::make($request->all(), [ + 'status' => 'required|in:aktif,tidak_aktif', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'errors' => $validator->errors() + ], 422); + } + + $akunTeknisi->update([ + 'status' => $request->status + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Status akun teknisi berhasil diupdate!' + ]); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal update status: ' . $e->getMessage() + ], 500); + } + } +} \ No newline at end of file diff --git a/samooapk/app/Http/Controllers/Api/AbsensiApiController.php b/samooapk/app/Http/Controllers/Api/AbsensiApiController.php new file mode 100644 index 0000000..737f9ca --- /dev/null +++ b/samooapk/app/Http/Controllers/Api/AbsensiApiController.php @@ -0,0 +1,577 @@ +all()); + + $status = $request->input('status', 'hadir'); + + $rules = [ + 'id_teknisi' => 'required|exists:teknisis,id_teknisi', + 'status' => 'nullable|in:hadir,izin,sakit', + 'keterangan' => 'nullable|string|max:255', + 'latitude' => 'nullable|string', + 'longitude' => 'nullable|string', + ]; + + if ($status === 'hadir') { + $rules['foto_absen_masuk'] = 'required|image|mimes:jpeg,png,jpg,gif|max:2048'; + } else { + $rules['foto_absen_masuk'] = 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048'; + } + + if ($status === 'izin') { + $rules['keterangan'] = 'required|string|max:255'; + } + + $validator = Validator::make($request->all(), $rules, [ + 'foto_absen_masuk.required' => 'Foto wajib untuk status Hadir', + 'keterangan.required' => 'Keterangan wajib untuk status Izin', + ]); + + if ($validator->fails()) { + Log::error('Validation failed:', $validator->errors()->toArray()); + return response()->json([ + 'success' => false, + 'message' => 'Validasi gagal', + 'errors' => $validator->errors() + ], 422); + } + + try { + // Cek apakah ada sesi yang masih aktif (belum absen keluar) + $activeAbsen = Absensi::where('id_teknisi', $request->id_teknisi) + ->whereNull('jam_keluar') + ->whereDate('tanggal', Carbon::today()) + ->first(); + + if ($activeAbsen) { + return response()->json([ + 'success' => false, + 'message' => 'Anda masih memiliki sesi absen yang aktif' + ], 400); + } + + $data = [ + 'id_teknisi' => $request->id_teknisi, + 'tanggal' => Carbon::now('Asia/Jakarta')->toDateString(), + 'jam_masuk' => $status === 'hadir' ? Carbon::now('Asia/Jakarta') : null, + 'status' => $status, + 'keterangan' => $request->keterangan, + 'latitude' => $request->latitude, + 'longitude' => $request->longitude, + ]; + + if ($request->hasFile('foto_absen_masuk')) { + $file = $request->file('foto_absen_masuk'); + $filename = uniqid() . '.' . $file->extension(); + $file->move(public_path('storage/absensi-masuk'), $filename); + $data['foto_absen_masuk'] = 'absensi-masuk/' . $filename; +} + + $absensi = Absensi::create($data); + $absensi->load('teknisi'); + + return response()->json([ + 'success' => true, + 'message' => 'Absen masuk berhasil dicatat', + 'data' => $absensi + ], 201); + + } catch (\Exception $e) { + Log::error('Error in absenMasuk: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Gagal melakukan absen masuk', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Absen keluar untuk teknisi dengan support status. + */ + public function absenKeluar(Request $request) + { + Log::info('Absen Keluar Request:', $request->all()); + + $status = $request->input('status', 'hadir'); + + $rules = [ + 'id_teknisi' => 'required|exists:teknisis,id_teknisi', + 'status' => 'nullable|in:hadir,izin,sakit', + 'keterangan' => 'nullable|string|max:255', + 'latitude' => 'nullable|string', + 'longitude' => 'nullable|string', + ]; + + if ($status === 'hadir') { + $rules['foto_absen_keluar'] = 'required|image|mimes:jpeg,png,jpg,gif|max:2048'; + } else { + $rules['foto_absen_keluar'] = 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048'; + } + + if ($status === 'izin') { + $rules['keterangan'] = 'required|string|max:255'; + } + + $validator = Validator::make($request->all(), $rules, [ + 'foto_absen_keluar.required' => 'Foto wajib untuk status Hadir', + 'keterangan.required' => 'Keterangan wajib untuk status Izin', + ]); + + if ($validator->fails()) { + Log::error('Validation failed:', $validator->errors()->toArray()); + return response()->json([ + 'success' => false, + 'message' => 'Validasi gagal', + 'errors' => $validator->errors() + ], 422); + } + + try { + // Cari sesi terbaru yang belum absen keluar + $absensi = Absensi::where('id_teknisi', $request->id_teknisi) + ->whereNull('jam_keluar') + ->orderBy('id_absensi', 'desc') + ->first(); + + if (!$absensi) { + return response()->json([ + 'success' => false, + 'message' => 'Tidak ada sesi absen aktif yang ditemukan' + ], 400); + } + + $data = ['jam_keluar' => Carbon::now('Asia/Jakarta')]; + + if ($request->has('status')) { + $data['status'] = $status; + } + + if ($request->has('keterangan')) { + $data['keterangan'] = $absensi->keterangan + ? $absensi->keterangan . ' | ' . $request->keterangan + : $request->keterangan; + } + + if ($request->has('latitude')) { + $data['latitude'] = $request->latitude; + } + + if ($request->has('longitude')) { + $data['longitude'] = $request->longitude; + } + + if ($request->hasFile('foto_absen_keluar')) { + $file = $request->file('foto_absen_keluar'); + $filename = uniqid() . '.' . $file->extension(); + $file->move(public_path('storage/absensi-keluar'), $filename); + $data['foto_absen_keluar'] = 'absensi-keluar/' . $filename; +} + + $absensi->update($data); + $absensi->load('teknisi'); + + return response()->json([ + 'success' => true, + 'message' => 'Absen keluar berhasil dicatat', + 'data' => $absensi + ], 200); + + } catch (\Exception $e) { + Log::error('Error in absenKeluar: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Gagal melakukan absen keluar', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Mengecek status absensi teknisi hari ini. + */ + public function checkStatus($id_teknisi) + { + try { + $teknisi = Teknisi::where('id_teknisi', $id_teknisi)->first(); + + if (!$teknisi) { + return response()->json([ + 'success' => false, + 'message' => 'Teknisi tidak ditemukan' + ], 404); + } + + // Ambil sesi terbaru hari ini + $absensi = Absensi::where('id_teknisi', $id_teknisi) + ->whereDate('tanggal', Carbon::today()) + ->orderBy('id_absensi', 'desc') + ->first(); + + $status = [ + 'sudah_absen_masuk' => false, + 'sudah_absen_keluar' => false, + 'data_absensi' => null, + ]; + + if ($absensi) { + $status['sudah_absen_masuk'] = !empty($absensi->jam_masuk) && empty($absensi->jam_keluar) && $absensi->status === 'hadir'; + $status['sudah_absen_keluar'] = !empty($absensi->jam_keluar); + $status['data_absensi'] = [ + 'jam_masuk' => $absensi->jam_masuk, + 'jam_keluar' => $absensi->jam_keluar, + 'jam_masuk_formatted' => $absensi->jam_masuk_formatted, + 'jam_keluar_formatted' => $absensi->jam_keluar_formatted, + 'durasi_kerja_formatted' => $absensi->durasi_kerja_formatted, + 'status' => $absensi->status, + 'keterangan' => $absensi->keterangan, + 'lokasi_masuk' => $absensi->lokasi_masuk ?? '-', + 'lokasi_valid' => $absensi->lokasi_valid ?? false, + 'latitude' => $absensi->latitude, + 'longitude' => $absensi->longitude, + 'foto_absen_masuk' => $absensi->foto_absen_masuk, + 'foto_absen_keluar' => $absensi->foto_absen_keluar, + ]; + } + + return response()->json([ + 'success' => true, + 'message' => 'Status absensi berhasil diambil', + 'data' => $status + ], 200); + + } catch (\Exception $e) { + Log::error('Error in checkStatus: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengecek status absensi', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Mendapatkan riwayat absensi teknisi per bulan + * dengan field yang sudah diformat untuk blade & Flutter. + */ + public function riwayat(Request $request) + { + try { + $id_teknisi = $request->query('id_teknisi'); + + if (!$id_teknisi) { + return response()->json([ + 'success' => false, + 'message' => 'id_teknisi diperlukan' + ], 400); + } + + $query = Absensi::where('id_teknisi', $id_teknisi); + + if ($request->has('bulan') && $request->has('tahun')) { + $query->filterByMonth($request->bulan, $request->tahun); + } + + $absensis = $query->orderBy('tanggal', 'desc')->get(); + + // ── Transform: kirim field yang sudah diformat ────────────── + $data = $absensis->map(function ($absensi) { + // Hitung menit telat (jadwal masuk 08:00) + $menitTelat = 0; + $terlambat = false; + + if ($absensi->jam_masuk && $absensi->status === 'hadir') { + $jamMasuk = Carbon::parse($absensi->jam_masuk)->setTimezone('Asia/Jakarta'); + $jamJadwal = Carbon::parse( + $absensi->tanggal->format('Y-m-d') . ' 08:00:00' + )->setTimezone('Asia/Jakarta'); + + if ($jamMasuk->gt($jamJadwal)) { + $terlambat = true; + $menitTelat = $jamMasuk->diffInMinutes($jamJadwal); + } + } + + return [ + 'tanggal' => $absensi->tanggal + ? $absensi->tanggal->format('Y-m-d') + : null, + 'status' => $absensi->status, + 'jam_masuk_formatted' => $absensi->jam_masuk_formatted, + 'jam_keluar_formatted' => $absensi->jam_keluar_formatted, + 'durasi_kerja_formatted' => $absensi->durasi_kerja_formatted, + 'terlambat' => $terlambat, + 'menit_telat' => $menitTelat, + 'keterangan' => $absensi->keterangan, + 'latitude' => $absensi->latitude, + 'longitude' => $absensi->longitude, + ]; + }); + + return response()->json([ + 'success' => true, + 'message' => 'Riwayat absensi berhasil diambil', + 'data' => $data + ], 200); + + } catch (\Exception $e) { + Log::error('Error in riwayat: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil riwayat absensi', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Mendapatkan statistik absensi. + */ + public function statistik(Request $request) + { + try { + $startDate = $request->input('start_date'); + $endDate = $request->input('end_date'); + $idTeknisi = $request->input('id_teknisi'); + + $query = Absensi::query(); + + if ($startDate && $endDate) { + $query->whereBetween('tanggal', [$startDate, $endDate]); + } + + if ($idTeknisi) { + $query->where('id_teknisi', $idTeknisi); + } + + $statistik = [ + 'total' => $query->count(), + 'hadir' => (clone $query)->where('status', 'hadir')->count(), + 'sakit' => (clone $query)->where('status', 'sakit')->count(), + 'izin' => (clone $query)->where('status', 'izin')->count(), + ]; + + return response()->json([ + 'success' => true, + 'message' => 'Statistik absensi berhasil diambil', + 'data' => $statistik + ], 200); + + } catch (\Exception $e) { + Log::error('Error in statistik: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil statistik absensi', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Mendapatkan daftar status absensi yang tersedia. + */ + public function getStatusOptions() + { + return response()->json([ + 'success' => true, + 'message' => 'Daftar status absensi berhasil diambil', + 'data' => [ + 'hadir' => 'Hadir', + 'izin' => 'Izin', + 'sakit' => 'Sakit', + ] + ], 200); + } + + /** + * Mendapatkan rekap absensi bulanan teknisi. + */ + public function rekap(Request $request) + { + try { + $id_teknisi = $request->query('id_teknisi'); + $bulan = (int) $request->query('bulan', date('n')); + $tahun = (int) $request->query('tahun', date('Y')); + + if (!$id_teknisi) { + return response()->json([ + 'success' => false, + 'message' => 'id_teknisi diperlukan' + ], 400); + } + + $absensis = Absensi::where('id_teknisi', $id_teknisi) + ->whereMonth('tanggal', $bulan) + ->whereYear('tanggal', $tahun) + ->get(); + + $hadir = $absensis->where('status', 'hadir')->count(); + $izin = $absensis->where('status', 'izin')->count(); + $sakit = $absensis->where('status', 'sakit')->count(); + $total = $absensis->count(); + + // Hitung persentase kehadiran + $persentase = $total > 0 ? round(($hadir / $total) * 100, 1) : 0; + + // Hitung rata-rata jam masuk + $hadirItems = $absensis->where('status', 'hadir') + ->filter(fn($a) => $a->jam_masuk !== null); + + $rataJamMasuk = '-'; + $rataJamKeluar = '-'; + $rataDurasi = '-'; + $terlambat = 0; + $streak = 0; + + if ($hadirItems->count() > 0) { + // Rata-rata masuk + $totalMasukMenit = $hadirItems->sum(function ($a) { + return Carbon::parse($a->jam_masuk) + ->setTimezone('Asia/Jakarta') + ->hour * 60 + + Carbon::parse($a->jam_masuk) + ->setTimezone('Asia/Jakarta') + ->minute; + }); + $avgMasuk = round($totalMasukMenit / $hadirItems->count()); + $rataJamMasuk = sprintf('%02d:%02d', intdiv($avgMasuk, 60), $avgMasuk % 60); + + // Rata-rata keluar + $keluarItems = $hadirItems->filter(fn($a) => $a->jam_keluar !== null); + if ($keluarItems->count() > 0) { + $totalKeluarMenit = $keluarItems->sum(function ($a) { + return Carbon::parse($a->jam_keluar) + ->setTimezone('Asia/Jakarta') + ->hour * 60 + + Carbon::parse($a->jam_keluar) + ->setTimezone('Asia/Jakarta') + ->minute; + }); + $avgKeluar = round($totalKeluarMenit / $keluarItems->count()); + $rataJamKeluar = sprintf('%02d:%02d', intdiv($avgKeluar, 60), $avgKeluar % 60); + + // Rata-rata durasi + $totalDurasiMenit = $keluarItems->sum(fn($a) => $a->durasi_kerja); + $avgDurasi = round($totalDurasiMenit / $keluarItems->count()); + $jam = intdiv($avgDurasi, 60); + $menit = $avgDurasi % 60; + $rataDurasi = "{$jam}j {$menit}m"; + } + + // Hitung keterlambatan + $jadwalMasuk = '08:00'; + $terlambat = $hadirItems->filter(function ($a) use ($jadwalMasuk) { + $jamMasuk = Carbon::parse($a->jam_masuk)->setTimezone('Asia/Jakarta'); + $jamJadwal = Carbon::parse( + $a->tanggal->format('Y-m-d') . ' ' . $jadwalMasuk + )->setTimezone('Asia/Jakarta'); + return $jamMasuk->gt($jamJadwal); + })->count(); + } + + // Hitung streak (berturut-turut hadir dari hari ini mundur) + $sortedDesc = $absensis->sortByDesc('tanggal'); + foreach ($sortedDesc as $a) { + if ($a->status === 'hadir') $streak++; + else break; + } + + // Nama bulan Indonesia + $namaBulan = [ + 1=>'Januari',2=>'Februari',3=>'Maret',4=>'April', + 5=>'Mei',6=>'Juni',7=>'Juli',8=>'Agustus', + 9=>'September',10=>'Oktober',11=>'November',12=>'Desember' + ]; + + return response()->json([ + 'success' => true, + 'message' => 'Rekap absensi berhasil diambil', + 'data' => [ + 'bulan' => ($namaBulan[$bulan] ?? $bulan) . ' ' . $tahun, + 'total_hari_kerja'=> $total, + 'hadir' => $hadir, + 'izin' => $izin, + 'sakit' => $sakit, + 'persentase' => $persentase, + 'rata_masuk' => $rataJamMasuk, + 'rata_keluar' => $rataJamKeluar, + 'rata_durasi' => $rataDurasi, + 'keterlambatan' => $terlambat, + 'streak' => $streak, + ] + ], 200); + + } catch (\Exception $e) { + Log::error('Error in rekap: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil rekap absensi', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Mendapatkan status absensi per tanggal dalam 1 bulan (untuk kalender). + * Response: { "1": "hadir", "5": "izin", "10": "alpha", ... } + */ + public function kalender(Request $request) + { + try { + $id_teknisi = $request->query('id_teknisi'); + $bulan = $request->query('bulan', date('n')); + $tahun = $request->query('tahun', date('Y')); + + if (!$id_teknisi) { + return response()->json([ + 'success' => false, + 'message' => 'id_teknisi diperlukan' + ], 400); + } + + $absensis = Absensi::where('id_teknisi', $id_teknisi) + ->whereMonth('tanggal', $bulan) + ->whereYear('tanggal', $tahun) + ->get(['tanggal', 'status']); + + // Map tanggal (angka) => status + $data = []; + foreach ($absensis as $absensi) { + $tgl = (int) Carbon::parse($absensi->tanggal)->format('j'); + $data[$tgl] = $absensi->status; + } + + return response()->json([ + 'success' => true, + 'message' => 'Data kalender berhasil diambil', + 'data' => $data + ], 200); + + } catch (\Exception $e) { + Log::error('Error in kalender: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil data kalender', + 'error' => $e->getMessage() + ], 500); + } + } +} \ No newline at end of file diff --git a/samooapk/app/Http/Controllers/Api/AkunTeknisiApiController.php b/samooapk/app/Http/Controllers/Api/AkunTeknisiApiController.php new file mode 100644 index 0000000..1a7bdd9 --- /dev/null +++ b/samooapk/app/Http/Controllers/Api/AkunTeknisiApiController.php @@ -0,0 +1,44 @@ +belongsTo(Teknisi::class, 'id_teknisi', 'id_teknisi'); + } + + // JWT Methods + public function getJWTIdentifier() + { + return $this->getKey(); + } + + public function getJWTCustomClaims() + { + return []; + } +} \ No newline at end of file diff --git a/samooapk/app/Http/Controllers/Api/DashboardApiController.php b/samooapk/app/Http/Controllers/Api/DashboardApiController.php new file mode 100644 index 0000000..7b3cf11 --- /dev/null +++ b/samooapk/app/Http/Controllers/Api/DashboardApiController.php @@ -0,0 +1,242 @@ +input('id_teknisi'); + + if (!$idTeknisi) { + return response()->json([ + 'success' => false, + 'message' => 'ID Teknisi tidak ditemukan' + ], 401); + } + + $teknisi = Teknisi::findOrFail($idTeknisi); + + // 1. Tugas Hari Ini / Aktif + $tugasAktif = Penugasan::where(function ($q) use ($idTeknisi) { + $q->where('id_teknisi', $idTeknisi) + ->orWhereHas('timTeknisi', function ($sq) use ($idTeknisi) { + $sq->where('id_teknisi', $idTeknisi); + }); + }) + ->whereIn('status_pekerjaan', ['belum_mulai', 'dalam_proses']) + ->count(); + + // 2. Gaji Bulan Berjalan (Estimasi Ongkos Kerja) + $now = Carbon::now(); + + // Cari semua penugasan yang melibatkan teknisi ini di bulan berjalan (eager loaded untuk performa) + $penugasans = Penugasan::where(function ($q) use ($idTeknisi) { + // (a) Teknisi utama penugasan + $q->where('id_teknisi', $idTeknisi) + // (b) Anggota tim yang hadir + ->orWhereHas('timTeknisi', function ($sq) use ($idTeknisi) { + $sq->where('id_teknisi', $idTeknisi) + ->where('status_kehadiran', 'hadir'); + }); + }) + ->where('status_pekerjaan', 'selesai') + ->where(function ($q) use ($now) { + // Filter berdasarkan bulan selesai (Prioritas: tanggal_diselesaikan, fallback ke updated_at) + $q->where(function ($q2) use ($now) { + $q2->whereNotNull('tanggal_diselesaikan') + ->whereMonth('tanggal_diselesaikan', $now->month) + ->whereYear('tanggal_diselesaikan', $now->year); + })->orWhere(function ($q2) use ($now) { + $q2->whereNull('tanggal_diselesaikan') + ->whereMonth('updated_at', $now->month) + ->whereYear('updated_at', $now->year); + }); + }) + ->with(['items.tarif', 'timTeknisi']) + ->get(); + + $estimasiGaji = 0; + $tugasSelesaiBulanIni = 0; + + foreach ($penugasans as $penugasan) { + $tugasSelesaiBulanIni++; + + // Hitung jumlah anggota tim yang hadir + $jumlahHadir = $penugasan->countTimHadir(); + if ($jumlahHadir === 0) { + $jumlahHadir = 1; + } + + // Hitung total ongkos dari penugasan_items + $totalOngkosTugas = 0; + if ($penugasan->items->count() > 0) { + foreach ($penugasan->items as $item) { + $itemTotal = (float) $item->total_nilai_pekerjaan; + if ($itemTotal <= 0) { + $itemTotal = $this->calculatePenugasanItemValue($item); + } + $totalOngkosTugas += $itemTotal; + } + } + + // Fallback: ambil dari total_nilai_pekerjaan penugasan induk atau tarif + if ($totalOngkosTugas <= 0) { + $totalOngkosTugas = $this->calculatePenugasanValue($penugasan); + } + + if ($totalOngkosTugas <= 0) { + continue; + } + + // Bagian ongkos = total ongkos dibagi jumlah anggota tim yang hadir + $bagianOngkos = $totalOngkosTugas / $jumlahHadir; + $estimasiGaji += $bagianOngkos; + } + + // 3. Total Kasbon Aktif + $totalKasbon = Kasbon::where('id_teknisi', $idTeknisi) + ->where('status', 'belum_lunas') + ->sum('jumlah_kasbon'); + + // 4. Gaji Terakhir Diterima + $gajiTerakhir = Penggajian::where('id_teknisi', $idTeknisi) + ->where('status_pembayaran', 'sudah_bayar') + ->orderBy('periode_tahun', 'desc') + ->orderBy('periode_bulan', 'desc') + ->first(); + + // 5. Statistik Absensi Bulan Ini + $absensiBulanIni = Absensi::where('id_teknisi', $idTeknisi) + ->whereMonth('tanggal', $now->month) + ->whereYear('tanggal', $now->year) + ->get(); + + $hadir = $absensiBulanIni->where('status', 'hadir')->count(); + $totalAbsen = $absensiBulanIni->count(); + $kehadiran = $totalAbsen > 0 ? round(($hadir / $totalAbsen) * 100) : 0; + + // Rata-rata jam kerja (dalam jam) + $totalDurasiMenit = $absensiBulanIni->where('status', 'hadir')->sum(function($a) { + return $a->durasi_kerja; + }); + $jamKerja = round($totalDurasiMenit / 60, 1); + + // Efisiensi + $poolTugas = $tugasAktif + $tugasSelesaiBulanIni; + $efisiensi = $poolTugas > 0 ? round(($tugasSelesaiBulanIni / $poolTugas) * 100) : 0; + + return response()->json([ + 'success' => true, + 'message' => 'Data dashboard berhasil diambil', + 'data' => [ + 'teknisi' => [ + 'nama' => $teknisi->nama, + 'spesialisasi' => $teknisi->spesialisasi, + 'foto' => $teknisi->foto_url + ], + 'statistik' => [ + 'tugas_aktif' => $tugasAktif, + 'tugas_selesai' => $tugasSelesaiBulanIni, + 'estimasi_gaji' => (float) $estimasiGaji, + 'total_kasbon' => (float) $totalKasbon, + 'gaji_terakhir' => $gajiTerakhir ? (float) $gajiTerakhir->gaji_bersih : 0, + 'periode_terakhir' => $gajiTerakhir ? Penggajian::getNamaBulan($gajiTerakhir->periode_bulan) . ' ' . $gajiTerakhir->periode_tahun : '-', + 'kehadiran' => $kehadiran, + 'jam_kerja' => $jamKerja, + 'efisiensi' => $efisiensi, + ] + ] + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil data dashboard: ' . $e->getMessage() + ], 500); + } + } + + /** + * Calculate nilai penugasan dengan fallback ke tarif + */ + private function calculatePenugasanValue($penugasan): float + { + if ($penugasan->total_nilai_pekerjaan > 0) { + return (float) $penugasan->total_nilai_pekerjaan; + } + + $tarif = $penugasan->tarif; + if (!$tarif) { + $query = TarifPekerjaan::where('jenis_pekerjaan', $penugasan->jenis_pekerjaan) + ->where('is_active', true); + if ($penugasan->dimensi_pipa) { + $query->where('dimensi_pipa', $penugasan->dimensi_pipa); + } + $tarif = $query->first(); + } + + if (!$tarif) return 0; + + if ($penugasan->jarak_meter > 0 && $tarif->tarif_per_meter) { + return (float) $tarif->tarif_per_meter * (float) $penugasan->jarak_meter; + } + if ($penugasan->jumlah_unit > 0 && $tarif->tarif_per_unit) { + return (float) $tarif->tarif_per_unit * (int) $penugasan->jumlah_unit; + } + if ($penugasan->jumlah_titik > 0 && $tarif->tarif_per_unit) { + return (float) $tarif->tarif_per_unit * (int) $penugasan->jumlah_titik; + } + + return (float) ($tarif->tarif_per_unit ?? $tarif->tarif_per_meter ?? 0); + } + + /** + * Calculate nilai untuk setiap PenugasanItem + */ + private function calculatePenugasanItemValue($item): float + { + if ($item->total_nilai_pekerjaan > 0) { + return (float) $item->total_nilai_pekerjaan; + } + + $tarif = $item->tarif; + if (!$tarif) { + $query = TarifPekerjaan::where('jenis_pekerjaan', $item->jenis_pekerjaan) + ->where('is_active', true); + if ($item->dimensi_pipa) { + $query->where('dimensi_pipa', $item->dimensi_pipa); + } + $tarif = $query->first(); + } + + if (!$tarif) return 0; + + if ($item->jarak_meter > 0 && $tarif->tarif_per_meter) { + return (float) $tarif->tarif_per_meter * (float) $item->jarak_meter; + } + if ($item->jumlah_unit > 0 && $tarif->tarif_per_unit) { + return (float) $tarif->tarif_per_unit * (int) $item->jumlah_unit; + } + if ($item->jumlah_titik > 0 && $tarif->tarif_per_unit) { + return (float) $tarif->tarif_per_unit * (int) $item->jumlah_titik; + } + + return (float) ($tarif->tarif_per_unit ?? $tarif->tarif_per_meter ?? 0); + } +} diff --git a/samooapk/app/Http/Controllers/Api/GajiApiController.php b/samooapk/app/Http/Controllers/Api/GajiApiController.php new file mode 100644 index 0000000..4430928 --- /dev/null +++ b/samooapk/app/Http/Controllers/Api/GajiApiController.php @@ -0,0 +1,116 @@ +input('id_teknisi'); + + if (!$idTeknisi) { + return response()->json([ + 'success' => false, + 'message' => 'ID Teknisi tidak ditemukan' + ], 401); + } + + $riwayat = Penggajian::where('id_teknisi', $idTeknisi) + ->orderBy('periode_tahun', 'desc') + ->orderBy('periode_bulan', 'desc') + ->paginate(12); + + $riwayat->getCollection()->transform(function ($item) { + return [ + 'id_penggajian' => $item->id_penggajian, + 'periode_bulan' => $item->periode_bulan, + 'periode_tahun' => $item->periode_tahun, + 'nama_bulan' => Penggajian::getNamaBulan($item->periode_bulan), + 'tanggal_penggajian' => $item->tanggal_penggajian->format('d M Y'), + 'total_ongkos' => (float) $item->total_ongkos_pekerjaan, + 'potongan_kasbon' => (float) $item->total_kasbon, + 'potongan_makan' => (float) $item->biaya_makan, + 'gaji_bersih' => (float) $item->gaji_bersih, + 'status_pembayaran' => $item->status_pembayaran, + 'is_paid' => $item->isPaid(), + ]; + }); + + return response()->json([ + 'success' => true, + 'message' => 'Riwayat gaji berhasil diambil', + 'data' => $riwayat + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil data: ' . $e->getMessage() + ], 500); + } + } + + /** + * GET - Detail slip gaji + */ + public function show(Request $request, $id) + { + try { + $idTeknisi = $request->input('id_teknisi'); + $penggajian = Penggajian::with(['detailPenggajian.penugasan'])->findOrFail($id); + + // Keamanan: Pastikan teknisi hanya bisa lihat gajinya sendiri + if ($idTeknisi && $penggajian->id_teknisi != $idTeknisi) { + return response()->json([ + 'success' => false, + 'message' => 'Akses ditolak' + ], 403); + } + + $details = $penggajian->detailPenggajian->map(function ($detail) { + return [ + 'id_penugasan' => $detail->id_penugasan, + 'pekerjaan' => $detail->penugasan->label_jenis_pekerjaan ?? 'Pekerjaan', + 'lokasi' => $detail->lokasi, + 'bagian_ongkos' => (float) $detail->bagian_ongkos, + 'rincian' => $detail->rincian_pekerjaan, + 'tanggal_selesai' => $detail->tanggal_selesai ? $detail->tanggal_selesai->format('d/m/Y') : '-', + ]; + }); + + return response()->json([ + 'success' => true, + 'message' => 'Detail gaji berhasil diambil', + 'data' => [ + 'header' => [ + 'id_penggajian' => $penggajian->id_penggajian, + 'periode' => Penggajian::getNamaBulan($penggajian->periode_bulan) . ' ' . $penggajian->periode_tahun, + 'tanggal_hitung' => $penggajian->tanggal_penggajian->format('d M Y'), + 'hari_kerja' => $penggajian->jumlah_hari_kerja, + 'gaji_kotor' => (float) $penggajian->total_ongkos_pekerjaan, + 'potongan_kasbon' => (float) $penggajian->total_kasbon, + 'potongan_makan' => (float) $penggajian->biaya_makan, + 'gaji_bersih' => (float) $penggajian->gaji_bersih, + 'status_pembayaran' => $penggajian->status_pembayaran, + ], + 'items' => $details + ] + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Data tidak ditemukan: ' . $e->getMessage() + ], 404); + } + } +} diff --git a/samooapk/app/Http/Controllers/Api/KasbonApiController.php b/samooapk/app/Http/Controllers/Api/KasbonApiController.php new file mode 100644 index 0000000..48fe3b6 --- /dev/null +++ b/samooapk/app/Http/Controllers/Api/KasbonApiController.php @@ -0,0 +1,96 @@ +input('id_teknisi'); + + if (!$idTeknisi) { + return response()->json([ + 'success' => false, + 'message' => 'ID Teknisi tidak ditemukan' + ], 401); + } + + $query = Kasbon::where('id_teknisi', $idTeknisi) + ->orderBy('tanggal_kasbon', 'desc'); + + if ($request->filled('status')) { + $query->where('status', $request->status); + } + + $riwayat = $query->paginate(15); + + $riwayat->getCollection()->transform(function ($item) { + return [ + 'id_kasbon' => $item->id_kasbon, + 'nominal' => (float) $item->jumlah_kasbon, + 'tanggal' => $item->tanggal_kasbon->format('d M Y'), + 'keperluan' => $item->keperluan, + 'detail' => $item->keterangan_detail, + 'status' => $item->status, + 'status_label' => $item->status == 'lunas' ? 'Lunas' : 'Belum Lunas', + ]; + }); + + return response()->json([ + 'success' => true, + 'message' => 'Riwayat kasbon berhasil diambil', + 'data' => $riwayat + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil data: ' . $e->getMessage() + ], 500); + } + } + + /** + * GET - Statistik kasbon (total hutang) + */ + public function statistik(Request $request) + { + try { + $idTeknisi = $request->input('id_teknisi'); + + if (!$idTeknisi) { + return response()->json([ + 'success' => false, + 'message' => 'ID Teknisi tidak ditemukan' + ], 401); + } + + $totalHutang = Kasbon::where('id_teknisi', $idTeknisi) + ->where('status', 'belum_lunas') + ->sum('jumlah_kasbon'); + + return response()->json([ + 'success' => true, + 'message' => 'Statistik kasbon berhasil diambil', + 'data' => [ + 'total_hutang' => (float) $totalHutang + ] + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil statistik: ' . $e->getMessage() + ], 500); + } + } +} diff --git a/samooapk/app/Http/Controllers/Api/PenugasanApiController.php b/samooapk/app/Http/Controllers/Api/PenugasanApiController.php new file mode 100644 index 0000000..c9c07b6 --- /dev/null +++ b/samooapk/app/Http/Controllers/Api/PenugasanApiController.php @@ -0,0 +1,854 @@ +input('id_teknisi'); + + if (!$idTeknisi) { + return response()->json([ + 'success' => false, + 'message' => 'ID Teknisi tidak ditemukan' + ], 401); + } + + $query = Penugasan::with(['teknisi', 'tarif', 'timTeknisi.teknisi', 'items.tarif']) + ->where(function ($q) use ($idTeknisi) { + $q->where('id_teknisi', $idTeknisi) + ->orWhereHas('timTeknisi', function ($sq) use ($idTeknisi) { + $sq->where('id_teknisi', $idTeknisi); + }); + }); + + if ($request->filled('status')) { + $query->where('status_pekerjaan', $request->status); + } + + if ($request->filled('jenis_pekerjaan')) { + $query->where('jenis_pekerjaan', $request->jenis_pekerjaan); + } + + if ($request->filled('tanggal_mulai')) { + $query->whereDate('tanggal_diberikan', '>=', $request->tanggal_mulai); + } + + if ($request->filled('tanggal_akhir')) { + $query->whereDate('tanggal_diberikan', '<=', $request->tanggal_akhir); + } + + $penugasan = $query->orderBy('tanggal_diberikan', 'desc')->paginate(15); + + $penugasan->getCollection()->transform(function ($item) { + $namaTim = $item->timTeknisi->map(function ($tt) { + return $tt->teknisi->nama ?? 'N/A'; + })->implode(', '); + + $item->nama_tim = !empty($namaTim) ? $namaTim : ($item->teknisi->nama ?? 'N/A'); + + if ($item->teknisi) { + $item->teknisi->nama = $item->nama_tim; + } + + $item->foto_surat_url = $item->foto_surat_url; + $item->foto_sebelum_url = $item->foto_sebelum_url; + $item->foto_sesudah_url = $item->foto_sesudah_url; + $item->label_jenis_pekerjaan = $item->label_jenis_pekerjaan; + return $item; + }); + + return response()->json([ + 'success' => true, + 'message' => 'Data penugasan berhasil diambil', + 'data' => $penugasan + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil data: ' . $e->getMessage() + ], 500); + } + } + + /** + * GET - Detail penugasan + */ + public function show($id) + { + try { + $penugasan = Penugasan::with(['teknisi', 'tarif', 'timTeknisi.teknisi', 'items.tarif']) + ->findOrFail($id); + + $teamMembers = $penugasan->timTeknisi->map(function ($tt) { + return $tt->teknisi->nama ?? null; + })->filter()->unique()->values(); + + $namaTim = $teamMembers->implode(', '); + + $data = $penugasan->toArray(); + + $fullTeamNames = !empty($namaTim) ? $namaTim : ($penugasan->teknisi->nama ?? 'N/A'); + + if (isset($data['teknisi'])) { + $data['teknisi']['nama'] = $fullTeamNames; + } + + $prefix = !empty($namaTim) ? "[Tim: $namaTim] " : ""; + $data['catatan_admin'] = $prefix . ($penugasan->catatan_admin ?? ''); + $data['instruksi_tambahan'] = $data['catatan_admin']; + + $data['foto_surat_url'] = $penugasan->foto_surat_url; + $data['foto_sebelum_url'] = $penugasan->foto_sebelum_url; + $data['foto_sesudah_url'] = $penugasan->foto_sesudah_url; + $data['label_jenis_pekerjaan'] = $penugasan->label_jenis_pekerjaan; + $data['is_garansi_aktif'] = $penugasan->isGaransiAktif(); + $data['sisa_hari_garansi'] = $penugasan->getSisaHariGaransi(); + + return response()->json([ + 'success' => true, + 'message' => 'Detail penugasan berhasil diambil', + 'data' => $data + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Data tidak ditemukan: ' . $e->getMessage() + ], 404); + } + } + + /** + * POST - Teknisi melengkapi detail pekerjaan via mobile (pertama kali) + */ + public function lengkapiDetail(Request $request, $id) + { + try { + $penugasan = Penugasan::findOrFail($id); + + $validator = Validator::make($request->all(), [ + 'items' => 'required|array|min:1', + 'items.*.jenis_pekerjaan' => 'required|string', + 'items.*.dimensi_pipa' => 'nullable', + 'items.*.jarak_meter' => 'nullable|numeric', + 'items.*.jumlah_unit' => 'nullable|integer', + 'items.*.jumlah_titik' => 'nullable|integer', + 'items.*.pakai_pipa_besi' => 'nullable', + 'items.*.jenis_pengangkatan' => 'nullable', + 'detail_pekerjaan' => 'nullable|string', + 'tanggal_mulai' => 'required|date', + 'tim_teknisi' => 'nullable|array', + 'foto_sebelum' => 'nullable|file|max:10240', + 'foto_sesudah' => 'nullable|file|max:10240', + 'foto_sebelum_base64' => 'nullable|string', + 'foto_sesudah_base64' => 'nullable|string', + ]); + + if ($validator->fails()) { + \Illuminate\Support\Facades\Log::error('Validation Fail in lengkapiDetail', $validator->errors()->toArray()); + return response()->json([ + 'success' => false, + 'message' => 'Validasi gagal', + 'errors' => $validator->errors() + ], 422); + } + + DB::beginTransaction(); + + \App\Models\PenugasanItem::where('id_penugasan', $penugasan->id_penugasan)->delete(); + + $fotoSebelum = $penugasan->foto_sebelum; + if ($request->hasFile('foto_sebelum')) { + $fotoSebelum = $request->file('foto_sebelum')->store('penugasan/foto-sebelum', 'public'); + } elseif ($request->foto_sebelum_base64) { + $fotoSebelum = $this->storeBase64($request->foto_sebelum_base64, 'penugasan/foto-sebelum'); + } + + $fotoSesudah = $penugasan->foto_sesudah; + if ($request->hasFile('foto_sesudah')) { + $fotoSesudah = $request->file('foto_sesudah')->store('penugasan/foto-sesudah', 'public'); + } elseif ($request->foto_sesudah_base64) { + $fotoSesudah = $this->storeBase64($request->foto_sesudah_base64, 'penugasan/foto-sesudah'); + } + + $totalNilaiPenugasan = 0; + $hasSR = false; + + foreach ($request->items as $itemData) { + $tarif = TarifPekerjaan::where('jenis_pekerjaan', $itemData['jenis_pekerjaan']) + ->where('is_active', true); + + if (!empty($itemData['dimensi_pipa'])) { + $tarif->where('dimensi_pipa', $itemData['dimensi_pipa']); + } + + if (isset($itemData['pakai_pipa_besi'])) { + $tarif->where('pakai_pipa_besi', $itemData['pakai_pipa_besi']); + } + + $tarif = $tarif->first(); + $nilaiItem = $this->hitungNilaiItem($tarif, $itemData); + $totalNilaiPenugasan += $nilaiItem; + + \App\Models\PenugasanItem::create([ + 'id_penugasan' => $penugasan->id_penugasan, + 'id_tarif' => $tarif ? $tarif->id_tarif : null, + 'jenis_pekerjaan' => $itemData['jenis_pekerjaan'], + 'dimensi_pipa' => $itemData['dimensi_pipa'] ?? null, + 'jarak_meter' => $itemData['jarak_meter'] ?? null, + 'jumlah_unit' => $itemData['jumlah_unit'] ?? null, + 'jumlah_titik' => $itemData['jumlah_titik'] ?? null, + 'pakai_pipa_besi' => $itemData['pakai_pipa_besi'] ?? null, + 'jenis_pengangkatan' => $itemData['jenis_pengangkatan'] ?? null, + 'total_nilai_pekerjaan' => $nilaiItem, + ]); + + if ($itemData['jenis_pekerjaan'] === 'sr') $hasSR = true; + } + + $firstItem = $request->items[0]; + + $penugasan->update([ + 'jenis_pekerjaan' => $firstItem['jenis_pekerjaan'], + 'dimensi_pipa' => $firstItem['dimensi_pipa'] ?? $penugasan->dimensi_pipa, + 'jarak_meter' => $firstItem['jarak_meter'] ?? $penugasan->jarak_meter, + 'jumlah_unit' => $firstItem['jumlah_unit'] ?? $penugasan->jumlah_unit, + 'pakai_pipa_besi' => array_key_exists('pakai_pipa_besi', $firstItem) + ? $firstItem['pakai_pipa_besi'] + : $penugasan->pakai_pipa_besi, + 'status_pekerjaan' => $penugasan->status_pekerjaan === 'belum_mulai' + ? 'dalam_proses' + : $penugasan->status_pekerjaan, + 'total_nilai_pekerjaan' => $totalNilaiPenugasan, + 'detail_pekerjaan' => $request->has('detail_pekerjaan') + ? $request->detail_pekerjaan + : $penugasan->detail_pekerjaan, + 'tanggal_mulai' => $request->tanggal_mulai, + 'foto_sebelum' => $fotoSebelum, + 'foto_sesudah' => $fotoSesudah, + ]); + + if ($hasSR) { + $penugasan->setGaransiMeteranAir($request->tanggal_mulai); + $penugasan->save(); + } + + if ($request->filled('tim_teknisi')) { + foreach ($request->tim_teknisi as $idTeknisiTambahan) { + $exists = TimTeknisiPenugasan::where('id_penugasan', $penugasan->id_penugasan) + ->where('id_teknisi', $idTeknisiTambahan) + ->exists(); + + if (!$exists) { + TimTeknisiPenugasan::create([ + 'id_penugasan' => $penugasan->id_penugasan, + 'id_teknisi' => $idTeknisiTambahan, + 'status_kehadiran' => 'hadir', + ]); + } + } + } + + DB::commit(); + + $penugasan->load(['teknisi', 'tarif', 'timTeknisi.teknisi', 'items.tarif']); + + return response()->json([ + 'success' => true, + 'message' => 'Detail pekerjaan berhasil dilengkapi!', + 'data' => $penugasan + ]); + + } catch (Exception $e) { + DB::rollBack(); + return response()->json([ + 'success' => false, + 'message' => 'Gagal melengkapi detail: ' . $e->getMessage() + ], 500); + } + } + + /** + * PUT - Update / edit detail pekerjaan yang sudah diisi sebelumnya (teknisi via mobile) + */ + public function updateDetail(Request $request, $id) + { + try { + $penugasan = Penugasan::with(['timTeknisi'])->findOrFail($id); + + $validator = Validator::make($request->all(), [ + 'id_teknisi' => 'required|integer', + 'items' => 'required|array|min:1', + 'items.*.id_penugasan_item' => 'nullable|integer', + 'items.*.jenis_pekerjaan' => 'required|string', + 'items.*.dimensi_pipa' => 'nullable', + 'items.*.jarak_meter' => 'nullable|numeric', + 'items.*.jumlah_unit' => 'nullable|integer', + 'items.*.jumlah_titik' => 'nullable|integer', + 'items.*.pakai_pipa_besi' => 'nullable', + 'items.*.jenis_pengangkatan' => 'nullable', + 'detail_pekerjaan' => 'nullable|string', + 'tanggal_mulai' => 'nullable|date', + 'tim_teknisi' => 'nullable|array', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validasi gagal', + 'errors' => $validator->errors() + ], 422); + } + + $idTeknisiEditor = $request->id_teknisi; + $isAssigned = ($penugasan->id_teknisi == $idTeknisiEditor) || + $penugasan->timTeknisi->pluck('id_teknisi')->contains($idTeknisiEditor); + + if (!$isAssigned) { + return response()->json([ + 'success' => false, + 'message' => 'Anda tidak berwenang mengedit penugasan ini' + ], 403); + } + + DB::beginTransaction(); + + \App\Models\PenugasanItem::where('id_penugasan', $penugasan->id_penugasan)->delete(); + + $hasSR = false; + $processedItemIds = []; + + foreach ($request->items as $itemData) { + $tarifQuery = TarifPekerjaan::where('jenis_pekerjaan', $itemData['jenis_pekerjaan']) + ->where('is_active', true); + + if (!empty($itemData['dimensi_pipa'])) { + $tarifQuery->where('dimensi_pipa', $itemData['dimensi_pipa']); + } + + if (isset($itemData['pakai_pipa_besi'])) { + $tarifQuery->where('pakai_pipa_besi', $itemData['pakai_pipa_besi']); + } + + $tarif = $tarifQuery->first(); + $nilaiItem = $this->hitungNilaiItem($tarif, $itemData); + + $created = \App\Models\PenugasanItem::create([ + 'id_penugasan' => $penugasan->id_penugasan, + 'id_tarif' => $tarif ? $tarif->id_tarif : null, + 'jenis_pekerjaan' => $itemData['jenis_pekerjaan'], + 'dimensi_pipa' => $itemData['dimensi_pipa'] ?? null, + 'jarak_meter' => $itemData['jarak_meter'] ?? null, + 'jumlah_unit' => $itemData['jumlah_unit'] ?? null, + 'jumlah_titik' => $itemData['jumlah_titik'] ?? null, + 'pakai_pipa_besi' => $itemData['pakai_pipa_besi'] ?? null, + 'jenis_pengangkatan' => $itemData['jenis_pengangkatan'] ?? null, + 'total_nilai_pekerjaan' => $nilaiItem, + ]); + $processedItemIds[] = $created->id_penugasan_item; + + if ($itemData['jenis_pekerjaan'] === 'sr') $hasSR = true; + } + + if ($request->has('detail_pekerjaan')) { + $penugasan->detail_pekerjaan = $request->detail_pekerjaan; + } + + if ($request->filled('tanggal_mulai')) { + $penugasan->tanggal_mulai = $request->tanggal_mulai; + } + + if ($request->filled('tim_teknisi')) { + foreach ($request->tim_teknisi as $idTeknisiTambahan) { + $exists = TimTeknisiPenugasan::where('id_penugasan', $penugasan->id_penugasan) + ->where('id_teknisi', $idTeknisiTambahan) + ->exists(); + + if (!$exists) { + TimTeknisiPenugasan::create([ + 'id_penugasan' => $penugasan->id_penugasan, + 'id_teknisi' => $idTeknisiTambahan, + 'status_kehadiran' => 'hadir', + ]); + } + } + } + + $total = \App\Models\PenugasanItem::where('id_penugasan', $penugasan->id_penugasan) + ->sum('total_nilai_pekerjaan'); + + $firstItem = $request->items[0]; + + $penugasan->total_nilai_pekerjaan = $total; + $penugasan->jenis_pekerjaan = $firstItem['jenis_pekerjaan'] ?? $penugasan->jenis_pekerjaan; + $penugasan->dimensi_pipa = $firstItem['dimensi_pipa'] ?? $penugasan->dimensi_pipa; + $penugasan->jarak_meter = $firstItem['jarak_meter'] ?? $penugasan->jarak_meter; + $penugasan->jumlah_unit = $firstItem['jumlah_unit'] ?? $penugasan->jumlah_unit; + $penugasan->pakai_pipa_besi = array_key_exists('pakai_pipa_besi', $firstItem) + ? $firstItem['pakai_pipa_besi'] + : $penugasan->pakai_pipa_besi; + + $statusBolehDiubah = ['belum_mulai']; + if (in_array($penugasan->status_pekerjaan, $statusBolehDiubah)) { + $penugasan->status_pekerjaan = 'dalam_proses'; + } + + $penugasan->save(); + + if ($hasSR) { + $penugasan->setGaransiMeteranAir($penugasan->tanggal_mulai ?? null); + $penugasan->save(); + } + + DB::commit(); + + $penugasan->load(['teknisi', 'tarif', 'timTeknisi.teknisi', 'items.tarif']); + + return response()->json([ + 'success' => true, + 'message' => 'Detail pekerjaan berhasil diperbarui!', + 'data' => $penugasan + ]); + + } catch (Exception $e) { + DB::rollBack(); + return response()->json([ + 'success' => false, + 'message' => 'Gagal memperbarui detail: ' . $e->getMessage() + ], 500); + } + } + + /** + * POST - Tambah rincian pekerjaan baru di tengah progres + */ + public function addItem(Request $request, $id) + { + try { + $penugasan = Penugasan::findOrFail($id); + + $validator = Validator::make($request->all(), [ + 'jenis_pekerjaan' => 'required|string', + 'dimensi_pipa' => 'nullable', + 'jarak_meter' => 'nullable|numeric', + 'jumlah_unit' => 'nullable|integer', + 'jumlah_titik' => 'nullable|integer', + 'pakai_pipa_besi' => 'nullable', + 'jenis_pengangkatan' => 'nullable', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validasi gagal', + 'errors' => $validator->errors() + ], 422); + } + + DB::beginTransaction(); + + $tarif = TarifPekerjaan::where('jenis_pekerjaan', $request->jenis_pekerjaan) + ->where('is_active', true); + + if ($request->filled('dimensi_pipa')) { + $tarif->where('dimensi_pipa', $request->dimensi_pipa); + } + + if ($request->has('pakai_pipa_besi')) { + $tarif->where('pakai_pipa_besi', $request->pakai_pipa_besi); + } + + $tarif = $tarif->first(); + $nilaiItem = $this->hitungNilaiItem($tarif, $request->all()); + + \App\Models\PenugasanItem::create([ + 'id_penugasan' => $penugasan->id_penugasan, + 'id_tarif' => $tarif ? $tarif->id_tarif : null, + 'jenis_pekerjaan' => $request->jenis_pekerjaan, + 'dimensi_pipa' => $request->dimensi_pipa, + 'jarak_meter' => $request->jarak_meter, + 'jumlah_unit' => $request->jumlah_unit, + 'jumlah_titik' => $request->jumlah_titik, + 'pakai_pipa_besi' => $request->pakai_pipa_besi, + 'jenis_pengangkatan' => $request->jenis_pengangkatan, + 'total_nilai_pekerjaan' => $nilaiItem, + ]); + + $total = \App\Models\PenugasanItem::where('id_penugasan', $penugasan->id_penugasan) + ->sum('total_nilai_pekerjaan'); + $penugasan->total_nilai_pekerjaan = $total; + $penugasan->save(); + + DB::commit(); + + return response()->json([ + 'success' => true, + 'message' => 'Rincian pekerjaan berhasil ditambahkan!', + 'data' => [ + 'nilai_item' => $nilaiItem, + 'total_baru' => $penugasan->total_nilai_pekerjaan + ] + ]); + + } catch (Exception $e) { + DB::rollBack(); + return response()->json([ + 'success' => false, + 'message' => 'Gagal menambah rincian: ' . $e->getMessage() + ], 500); + } + } + + /** + * PUT - Update status pekerjaan + * + * ✅ FIX: Ketika status diubah menjadi 'selesai', semua anggota tim + * otomatis dicatat status_kehadiran = 'hadir' di tabel tim_teknisi_penugasans. + * Ini memastikan semua anggota tim terhitung gajinya meskipun + * bukan dia yang menerima/menceklis tugas di awal. + */ + public function updateStatus(Request $request, $id) + { + try { + $penugasan = Penugasan::findOrFail($id); + + $validator = Validator::make($request->all(), [ + 'status_pekerjaan' => 'required|in:belum_mulai,dalam_proses,selesai,dibatalkan', + 'tanggal_diselesaikan' => 'required_if:status_pekerjaan,selesai|nullable|date', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validasi gagal', + 'errors' => $validator->errors() + ], 422); + } + + DB::beginTransaction(); + + $updateData = ['status_pekerjaan' => $request->status_pekerjaan]; + + if ($request->status_pekerjaan === 'selesai') { + $updateData['tanggal_diselesaikan'] = $request->tanggal_diselesaikan ?? now(); + + // ✅ FIX: Pastikan semua anggota tim tercatat hadir + // sehingga gaji semua anggota tim terhitung dengan benar + TimTeknisiPenugasan::where('id_penugasan', $penugasan->id_penugasan) + ->update(['status_kehadiran' => 'hadir']); + + // Recalculate total_nilai_pekerjaan if it's empty or zero + if (empty($penugasan->total_nilai_pekerjaan) || $penugasan->total_nilai_pekerjaan <= 0) { + $penugasan->load(['items.tarif']); + $totalNilai = 0; + + if ($penugasan->items && $penugasan->items->count() > 0) { + foreach ($penugasan->items as $item) { + $itemTotal = (float) $item->total_nilai_pekerjaan; + if ($itemTotal <= 0) { + $tarif = $item->tarif; + if (!$tarif) { + $tarifQuery = TarifPekerjaan::where('jenis_pekerjaan', $item->jenis_pekerjaan) + ->where('is_active', true); + if ($item->dimensi_pipa) { + $tarifQuery->where('dimensi_pipa', $item->dimensi_pipa); + } + $tarif = $tarifQuery->first(); + } + $itemTotal = $this->hitungNilaiItem($tarif, $item->toArray()); + if ($itemTotal > 0) { + $item->update(['total_nilai_pekerjaan' => $itemTotal]); + } + } + $totalNilai += $itemTotal; + } + } + + if ($totalNilai <= 0 && $penugasan->jenis_pekerjaan) { + $tarif = $penugasan->tarif; + if (!$tarif) { + $tarifQuery = TarifPekerjaan::where('jenis_pekerjaan', $penugasan->jenis_pekerjaan) + ->where('is_active', true); + if ($penugasan->dimensi_pipa) { + $tarifQuery->where('dimensi_pipa', $penugasan->dimensi_pipa); + } + $tarif = $tarifQuery->first(); + } + if ($tarif) { + if ($penugasan->jarak_meter > 0 && $tarif->tarif_per_meter) { + $totalNilai = (float) $tarif->tarif_per_meter * (float) $penugasan->jarak_meter; + } elseif ($penugasan->jumlah_unit > 0 && $tarif->tarif_per_unit) { + $totalNilai = (float) $tarif->tarif_per_unit * (int) $penugasan->jumlah_unit; + } elseif ($penugasan->jumlah_titik > 0 && $tarif->tarif_per_unit) { + $totalNilai = (float) $tarif->tarif_per_unit * (int) $penugasan->jumlah_titik; + } else { + $totalNilai = (float) ($tarif->tarif_per_unit ?? $tarif->tarif_per_meter ?? 0); + } + } + } + + if ($totalNilai > 0) { + $updateData['total_nilai_pekerjaan'] = $totalNilai; + } + } + } + + $penugasan->update($updateData); + + DB::commit(); + + return response()->json([ + 'success' => true, + 'message' => 'Status pekerjaan berhasil diupdate!', + 'data' => $penugasan + ]); + + } catch (Exception $e) { + DB::rollBack(); + return response()->json([ + 'success' => false, + 'message' => 'Gagal update status: ' . $e->getMessage() + ], 500); + } + } + + /** + * POST - Upload foto sebelum/sesudah + */ + public function uploadFoto(Request $request, $id) + { + try { + $penugasan = Penugasan::findOrFail($id); + + \Illuminate\Support\Facades\Log::info('Upload Foto Request Received', [ + 'id_penugasan' => $id, + 'tipe_foto' => $request->tipe_foto, + 'has_file' => $request->hasFile('foto'), + ]); + + $validator = Validator::make($request->all(), [ + 'tipe_foto' => 'required|in:sebelum,sesudah', + 'foto' => 'nullable|image|mimes:jpeg,png,jpg|max:10240', + 'sebelum_base64' => 'nullable|string', + 'sesudah_base64' => 'nullable|string', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validasi gagal', + 'errors' => $validator->errors() + ], 422); + } + + $tipeFoto = $request->tipe_foto; + $fotoPath = null; + + if ($request->hasFile('foto')) { + $fotoPath = $request->file('foto')->store("penugasan/foto-{$tipeFoto}", 'public'); + } elseif ($request->input($tipeFoto . '_base64')) { + $fotoPath = $this->storeBase64($request->input($tipeFoto . '_base64'), "penugasan/foto-{$tipeFoto}"); + } + + if (!$fotoPath) { + return response()->json([ + 'success' => false, + 'message' => 'Tidak ada foto yang diupload' + ], 422); + } + + if ($tipeFoto === 'sebelum') { + $penugasan->foto_sebelum = $fotoPath; + } else { + $penugasan->foto_sesudah = $fotoPath; + } + $penugasan->save(); + + return response()->json([ + 'success' => true, + 'message' => "Foto {$tipeFoto} berhasil diupload!", + 'data' => [ + 'foto_url' => asset("storage/{$fotoPath}"), + 'foto_path' => $fotoPath + ] + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal upload foto: ' . $e->getMessage() + ], 500); + } + } + + /** + * GET - Tarif berdasarkan jenis pekerjaan + */ + public function getTarifByJenis(Request $request) + { + try { + $validator = Validator::make($request->all(), [ + 'jenis_pekerjaan' => 'required|in:sr,pengembangan_jaringan_pipa,pengangkatan,pemasangan_gate_valve,gali_urug,perbaikan_jaringan_pipa,pengecatan_pipa_besi,penyempurnaan_jaringan_pipa', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Jenis pekerjaan tidak valid', + 'errors' => $validator->errors() + ], 422); + } + + $tarifs = TarifPekerjaan::where('jenis_pekerjaan', $request->jenis_pekerjaan) + ->where('is_active', true) + ->get(); + + return response()->json([ + 'success' => true, + 'message' => 'Data tarif berhasil diambil', + 'data' => $tarifs + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil data tarif: ' . $e->getMessage() + ], 500); + } + } + + /** + * GET - Statistik penugasan teknisi + */ + public function statistik(Request $request) + { + try { + $idTeknisi = $request->input('id_teknisi'); + + if (!$idTeknisi) { + return response()->json([ + 'success' => false, + 'message' => 'ID Teknisi tidak ditemukan' + ], 401); + } + + $baseQuery = Penugasan::where(function ($q) use ($idTeknisi) { + $q->where('id_teknisi', $idTeknisi) + ->orWhereHas('timTeknisi', function ($sq) use ($idTeknisi) { + $sq->where('id_teknisi', $idTeknisi); + }); + }); + + $statistik = [ + 'total_penugasan' => (clone $baseQuery)->count(), + 'belum_mulai' => (clone $baseQuery)->where('status_pekerjaan', 'belum_mulai')->count(), + 'dalam_proses' => (clone $baseQuery)->where('status_pekerjaan', 'dalam_proses')->count(), + 'selesai' => (clone $baseQuery)->where('status_pekerjaan', 'selesai')->count(), + 'menunggu_detail' => (clone $baseQuery)->whereNull('jenis_pekerjaan')->count(), + 'detail_lengkap' => (clone $baseQuery)->whereNotNull('jenis_pekerjaan')->count(), + ]; + + return response()->json([ + 'success' => true, + 'message' => 'Statistik berhasil diambil', + 'data' => $statistik + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil statistik: ' . $e->getMessage() + ], 500); + } + } + + /** + * GET - Daftar teknisi aktif untuk tambah tim + */ + public function getTeknisiList() + { + try { + $teknisi = Teknisi::where('status', 'aktif') + ->orderBy('nama') + ->get(['id_teknisi', 'nama', 'no_telepon', 'spesialisasi']); + + return response()->json([ + 'success' => true, + 'message' => 'Daftar teknisi berhasil diambil', + 'data' => $teknisi + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil data teknisi: ' . $e->getMessage() + ], 500); + } + } + + // =================================== + // PRIVATE HELPER + // =================================== + + private function hitungNilaiItem($tarif, array $data): float + { + if (!$tarif) return 0; + + if ($tarif->tarif_per_meter && !empty($data['jarak_meter'])) { + return (float)$tarif->tarif_per_meter * (float)$data['jarak_meter']; + } + + if ($tarif->tarif_per_unit && !empty($data['jumlah_unit'])) { + return (float)$tarif->tarif_per_unit * (int)$data['jumlah_unit']; + } + + if ($tarif->tarif_per_unit && !empty($data['jumlah_titik'])) { + return (float)$tarif->tarif_per_unit * (int)$data['jumlah_titik']; + } + + return (float)($tarif->tarif_per_unit ?? 0); + } + + private function storeBase64($base64String, $folder) + { + try { + if (preg_match("/^data:image\/(\w+);base64,/", $base64String, $type)) { + $base64String = substr($base64String, strpos($base64String, ",") + 1); + $type = strtolower($type[1]); + } else { + $type = "jpg"; + } + + $image = base64_decode($base64String); + if ($image === false) return null; + + $fileName = \Illuminate\Support\Str::random(40) . "." . $type; + $path = $folder . "/" . $fileName; + + \Illuminate\Support\Facades\Storage::disk("public")->put($path, $image); + + return $path; + } catch (Exception $e) { + \Illuminate\Support\Facades\Log::error("Base64 Store Error: " . $e->getMessage()); + return null; + } + } +} \ No newline at end of file diff --git a/samooapk/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/samooapk/app/Http/Controllers/Auth/AuthenticatedSessionController.php new file mode 100644 index 0000000..288b436 --- /dev/null +++ b/samooapk/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -0,0 +1,54 @@ +authenticate(); + + $request->session()->regenerate(); + + // Redirect ke dashboard kamu (bukan RouteServiceProvider::HOME) + return redirect()->intended(route('dashboard')); + } + + /** + * Destroy an authenticated session. + */ + public function destroy(Request $request): RedirectResponse + { + Auth::guard('web')->logout(); + + $request->session()->invalidate(); + + $request->session()->regenerateToken(); + + if ($request->query('to') === 'login') { + return redirect()->route('login'); + } + + // Redirect ke halaman home/welcome setelah logout + return redirect('/'); + } +} \ No newline at end of file diff --git a/samooapk/app/Http/Controllers/Auth/ConfirmablePasswordController.php b/samooapk/app/Http/Controllers/Auth/ConfirmablePasswordController.php new file mode 100644 index 0000000..523ddda --- /dev/null +++ b/samooapk/app/Http/Controllers/Auth/ConfirmablePasswordController.php @@ -0,0 +1,41 @@ +validate([ + 'email' => $request->user()->email, + 'password' => $request->password, + ])) { + throw ValidationException::withMessages([ + 'password' => __('auth.password'), + ]); + } + + $request->session()->put('auth.password_confirmed_at', time()); + + return redirect()->intended(RouteServiceProvider::HOME); + } +} diff --git a/samooapk/app/Http/Controllers/Auth/EmailVerificationNotificationController.php b/samooapk/app/Http/Controllers/Auth/EmailVerificationNotificationController.php new file mode 100644 index 0000000..96ba772 --- /dev/null +++ b/samooapk/app/Http/Controllers/Auth/EmailVerificationNotificationController.php @@ -0,0 +1,25 @@ +user()->hasVerifiedEmail()) { + return redirect()->intended(RouteServiceProvider::HOME); + } + + $request->user()->sendEmailVerificationNotification(); + + return back()->with('status', 'verification-link-sent'); + } +} diff --git a/samooapk/app/Http/Controllers/Auth/EmailVerificationPromptController.php b/samooapk/app/Http/Controllers/Auth/EmailVerificationPromptController.php new file mode 100644 index 0000000..186eb97 --- /dev/null +++ b/samooapk/app/Http/Controllers/Auth/EmailVerificationPromptController.php @@ -0,0 +1,22 @@ +user()->hasVerifiedEmail() + ? redirect()->intended(RouteServiceProvider::HOME) + : view('auth.verify-email'); + } +} diff --git a/samooapk/app/Http/Controllers/Auth/NewPasswordController.php b/samooapk/app/Http/Controllers/Auth/NewPasswordController.php new file mode 100644 index 0000000..f1e2814 --- /dev/null +++ b/samooapk/app/Http/Controllers/Auth/NewPasswordController.php @@ -0,0 +1,61 @@ + $request]); + } + + /** + * Handle an incoming new password request. + * + * @throws \Illuminate\Validation\ValidationException + */ + public function store(Request $request): RedirectResponse + { + $request->validate([ + 'token' => ['required'], + 'email' => ['required', 'email'], + 'password' => ['required', 'confirmed', Rules\Password::defaults()], + ]); + + // Here we will attempt to reset the user's password. If it is successful we + // will update the password on an actual user model and persist it to the + // database. Otherwise we will parse the error and return the response. + $status = Password::reset( + $request->only('email', 'password', 'password_confirmation', 'token'), + function ($user) use ($request) { + $user->forceFill([ + 'password' => Hash::make($request->password), + 'remember_token' => Str::random(60), + ])->save(); + + event(new PasswordReset($user)); + } + ); + + // If the password was successfully reset, we will redirect the user back to + // the application's home authenticated view. If there is an error we can + // redirect them back to where they came from with their error message. + return $status == Password::PASSWORD_RESET + ? redirect()->route('login')->with('status', __($status)) + : back()->withInput($request->only('email')) + ->withErrors(['email' => __($status)]); + } +} diff --git a/samooapk/app/Http/Controllers/Auth/PasswordController.php b/samooapk/app/Http/Controllers/Auth/PasswordController.php new file mode 100644 index 0000000..6916409 --- /dev/null +++ b/samooapk/app/Http/Controllers/Auth/PasswordController.php @@ -0,0 +1,29 @@ +validateWithBag('updatePassword', [ + 'current_password' => ['required', 'current_password'], + 'password' => ['required', Password::defaults(), 'confirmed'], + ]); + + $request->user()->update([ + 'password' => Hash::make($validated['password']), + ]); + + return back()->with('status', 'password-updated'); + } +} diff --git a/samooapk/app/Http/Controllers/Auth/PasswordResetLinkController.php b/samooapk/app/Http/Controllers/Auth/PasswordResetLinkController.php new file mode 100644 index 0000000..ce813a6 --- /dev/null +++ b/samooapk/app/Http/Controllers/Auth/PasswordResetLinkController.php @@ -0,0 +1,44 @@ +validate([ + 'email' => ['required', 'email'], + ]); + + // We will send the password reset link to this user. Once we have attempted + // to send the link, we will examine the response then see the message we + // need to show to the user. Finally, we'll send out a proper response. + $status = Password::sendResetLink( + $request->only('email') + ); + + return $status == Password::RESET_LINK_SENT + ? back()->with('status', __($status)) + : back()->withInput($request->only('email')) + ->withErrors(['email' => __($status)]); + } +} diff --git a/samooapk/app/Http/Controllers/Auth/RegisteredUserController.php b/samooapk/app/Http/Controllers/Auth/RegisteredUserController.php new file mode 100644 index 0000000..9f1a37b --- /dev/null +++ b/samooapk/app/Http/Controllers/Auth/RegisteredUserController.php @@ -0,0 +1,59 @@ +route('login')->with('error', 'Pendaftaran ditutup karena akun administrator sudah terdaftar.'); + } + + return view('auth.register'); + } + + /** + * Handle an incoming registration request. + * + * @throws \Illuminate\Validation\ValidationException + */ + public function store(Request $request): RedirectResponse + { + if (User::exists()) { + return redirect()->route('login')->with('error', 'Pendaftaran ditutup karena akun administrator sudah terdaftar.'); + } + + $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], + 'password' => ['required', 'confirmed', Rules\Password::defaults()], + ]); + + $user = User::create([ + 'name' => $request->name, + 'email' => $request->email, + 'password' => Hash::make($request->password), + ]); + + event(new Registered($user)); + + Auth::login($user); + + return redirect()->route('verification.notice'); + } +} diff --git a/samooapk/app/Http/Controllers/Auth/VerifyEmailController.php b/samooapk/app/Http/Controllers/Auth/VerifyEmailController.php new file mode 100644 index 0000000..ea87940 --- /dev/null +++ b/samooapk/app/Http/Controllers/Auth/VerifyEmailController.php @@ -0,0 +1,28 @@ +user()->hasVerifiedEmail()) { + return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); + } + + if ($request->user()->markEmailAsVerified()) { + event(new Verified($request->user())); + } + + return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); + } +} diff --git a/samooapk/app/Http/Controllers/Controller.php b/samooapk/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..858c1b1 --- /dev/null +++ b/samooapk/app/Http/Controllers/Controller.php @@ -0,0 +1,13 @@ +count(); // sesuaikan nanti kalau error +$totalPekerjaan = Penugasan::count(); +$totalLaporan = 0; +$penugasanAktif = Penugasan::where('status_pekerjaan', 'dalam_proses')->count(); // ✅ + + // ── BAR CHART & DONUT ──────────────────────────────── + $chartData = PenugasanItem::select('jenis_pekerjaan', DB::raw('count(*) as total')) + ->groupBy('jenis_pekerjaan') + ->pluck('total', 'jenis_pekerjaan'); + + $jenisMapping = [ + 'sr' => 'Sambungan Rumah (SR)', + 'pengembangan_jaringan_pipa' => 'Pengembangan Pipa', + 'perbaikan_jaringan_pipa' => 'Perbaikan Pipa', + 'gali_urug' => 'Gali Urug', + 'pemasangan_gate_valve' => 'Pemas. Gate Valve', + 'pengangkatan' => 'Pengangkatan', + 'pengecatan_pipa_besi' => 'Pengecatan Pipa', + 'penyempurnaan_jaringan_pipa' => 'Penyempurnaan Pipa', + ]; + + $chartColors = ['#1d9e75', '#378add', '#e24b4a', '#ef9f27', '#7f77dd', '#a552cc', '#34d399', '#f472b6']; + + $chartLabels = []; + $chartValues = []; + $chartBgColors = []; + + $i = 0; + foreach($jenisMapping as $key => $label) { + $chartLabels[] = $label; + $chartValues[] = $chartData->get($key, 0); + $chartBgColors[] = $chartColors[$i % count($chartColors)]; + $i++; + } + + // ── LINE CHART TEKNISI (MULTI-LINE PER BULAN) ────────────────── + $months = []; + $monthLabels = []; + for ($i = 5; $i >= 0; $i--) { + $date = Carbon::now()->subMonths($i); + $months[] = $date->format('Y-m'); + $monthLabels[] = $date->translatedFormat('F'); // ex: Januari, Februari + } + + $teknisiData = Penugasan::join('teknisis', 'penugasans.id_teknisi', '=', 'teknisis.id_teknisi') + ->select('teknisis.nama', DB::raw('DATE_FORMAT(penugasans.created_at, "%Y-%m") as month_year'), DB::raw('count(penugasans.id_penugasan) as total')) + ->where('penugasans.created_at', '>=', Carbon::now()->subMonths(5)->startOfMonth()) + ->groupBy('teknisis.id_teknisi', 'teknisis.nama', 'month_year') + ->get(); + + $teknisiDatasets = []; + $lineColors = ['#378add', '#e24b4a', '#1d9e75', '#ef9f27', '#7f77dd', '#a552cc', '#f472b6', '#34d399']; + $colorIdx = 0; + + foreach ($teknisiData->groupBy('nama') as $nama => $records) { + $dataArray = []; + foreach ($months as $m) { + $record = $records->firstWhere('month_year', $m); + $dataArray[] = $record ? $record->total : 0; + } + + $teknisiDatasets[] = [ + 'label' => $nama, + 'data' => $dataArray, + 'borderColor' => $lineColors[$colorIdx % count($lineColors)], + 'backgroundColor' => 'transparent', + 'borderWidth' => 2, + 'pointBackgroundColor' => '#fff', + 'pointBorderColor' => $lineColors[$colorIdx % count($lineColors)], + 'pointBorderWidth' => 2, + 'pointRadius' => 4, + 'tension' => 0.3 + ]; + $colorIdx++; + } + + // ── TARIF PEKERJAAN ────────────────────────────────── + $tarifPekerjaans = TarifPekerjaan::where('is_active', true) + ->orderBy('jenis_pekerjaan') + ->orderBy('dimensi_pipa') + ->get(); + + // ── KONTRAK — tidak ada, kirim collection kosong ───── + $kontrakJatuhTempo = collect(); + + return view('dashboard', compact( + 'totalTeknisi', + 'teknisiAktif', + 'totalPekerjaan', + 'totalLaporan', + 'penugasanAktif', + 'chartLabels', + 'chartValues', + 'chartBgColors', + 'monthLabels', + 'teknisiDatasets', + 'tarifPekerjaans', + 'kontrakJatuhTempo', + )); + } +} \ No newline at end of file diff --git a/samooapk/app/Http/Controllers/KasbonController.php b/samooapk/app/Http/Controllers/KasbonController.php new file mode 100644 index 0000000..3baa885 --- /dev/null +++ b/samooapk/app/Http/Controllers/KasbonController.php @@ -0,0 +1,515 @@ +has('status') && $request->status != '') { + $query->byStatus($request->status); + } + + // Filter berdasarkan teknisi jika ada + if ($request->has('id_teknisi') && $request->id_teknisi != '') { + $query->where('id_teknisi', $request->id_teknisi); + } + + // Filter berdasarkan tanggal + if ($request->has('tanggal_dari') && $request->tanggal_dari != '') { + $query->whereDate('tanggal_kasbon', '>=', $request->tanggal_dari); + } + + if ($request->has('tanggal_sampai') && $request->tanggal_sampai != '') { + $query->whereDate('tanggal_kasbon', '<=', $request->tanggal_sampai); + } + + // Sorting + $sortBy = $request->get('sort_by', 'created_at'); + $sortOrder = $request->get('sort_order', 'desc'); + $query->orderBy($sortBy, $sortOrder); + + // Pagination + $perPage = $request->get('per_page', 15); + $kasbons = $query->paginate($perPage); + + // Statistik (Disederhanakan untuk efisiensi) + $totalKasbon = Kasbon::count(); + $totalNominal = Kasbon::sum('jumlah_kasbon'); + $kasbonLunas = Kasbon::where('status', 'lunas')->count(); + $kasbonBelumLunas = Kasbon::where('status', 'belum_lunas')->count(); + $totalNominalBelumLunas = Kasbon::where('status', 'belum_lunas')->sum('jumlah_kasbon'); + + // Daftar teknisi untuk dropdown modal & filter + $teknisis = Teknisi::orderBy('nama')->get(); + + // Untuk API response + if ($request->expectsJson()) { + return response()->json([ + 'success' => true, + 'data' => $kasbons, + 'message' => 'Data kasbon berhasil diambil' + ]); + } + + return view('Admin.Gaji.Kasbon', compact( + 'kasbons', + 'totalKasbon', + 'totalNominal', + 'kasbonLunas', + 'kasbonBelumLunas', + 'totalNominalBelumLunas', + 'teknisis' + )); + } + + /** + * Show the form for creating a new resource. + * + * @return View + */ + public function create(): View + { + $statusOptions = Kasbon::getStatusOptions(); + return view('Admin.Gaji.create-kasbon', compact('statusOptions')); // DIPERBAIKI: path view + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * @return RedirectResponse|JsonResponse + */ + public function store(Request $request) + { + \Illuminate\Support\Facades\Log::info('Kasbon Store Request Received', $request->all()); + $validator = Validator::make($request->all(), [ + 'id_teknisi' => 'required|integer|min:1', + 'jumlah_kasbon' => 'required|numeric|min:0', + 'tanggal_kasbon' => 'required|date', + 'status' => 'nullable|in:lunas,belum_lunas', + 'keterangan' => 'nullable|string|max:100' + ], [ + 'id_teknisi.required' => 'ID Teknisi harus diisi', + 'id_teknisi.integer' => 'ID Teknisi harus berupa angka', + 'jumlah_kasbon.required' => 'Jumlah kasbon harus diisi', + 'jumlah_kasbon.numeric' => 'Jumlah kasbon harus berupa angka', + 'jumlah_kasbon.min' => 'Jumlah kasbon minimal 0', + 'tanggal_kasbon.required' => 'Tanggal kasbon harus diisi', + 'tanggal_kasbon.date' => 'Format tanggal kasbon tidak valid', + 'status.required' => 'Status harus dipilih', + 'status.in' => 'Status harus lunas atau belum_lunas', + 'keterangan.max' => 'Keterangan maksimal 500 karakter' + ]); + + $validator->after(function ($validator) use ($request) { + $jumlah = (float) $request->input('jumlah_kasbon'); + $idTeknisi = $request->input('id_teknisi'); + $tanggal = $request->input('tanggal_kasbon'); + + if ($idTeknisi) { + $hasWork = Penugasan::where(function ($q) use ($idTeknisi) { + $q->where('id_teknisi', $idTeknisi) + ->orWhereHas('timTeknisi', function ($sq) use ($idTeknisi) { + $sq->where('id_teknisi', $idTeknisi) + ->where('status_kehadiran', 'hadir'); + }); + })->where('status_pekerjaan', 'selesai')->exists(); + + if (!$hasWork) { + $validator->errors()->add('id_teknisi', 'Teknisi belum menyelesaikan pekerjaan apapun. Harus menyelesaikan minimal satu pekerjaan sebelum kasbon.'); + } + } + + if ($jumlah > 0 && $jumlah <= 500000) { + // Aturan 1: Minimal Rp 200.000 untuk Kasbon Rutin + if ($jumlah < 200000) { + $validator->errors()->add('jumlah_kasbon', 'Jumlah kasbon rutin minimal Rp 200.000. Di atas Rp 500.000 dianggap pinjaman besar.'); + } + + // Aturan 2: Maksimal 2 kali kasbon rutin dalam 1 minggu kalender + if ($idTeknisi && $tanggal) { + try { + $date = Carbon::parse($tanggal); + $startOfWeek = $date->copy()->startOfWeek()->toDateString(); + $endOfWeek = $date->copy()->endOfWeek()->toDateString(); + + $kasbonCount = Kasbon::where('id_teknisi', $idTeknisi) + ->where('jumlah_kasbon', '<=', 500000) + ->whereBetween('tanggal_kasbon', [$startOfWeek, $endOfWeek]) + ->count(); + + if ($kasbonCount >= 2) { + $validator->errors()->add('tanggal_kasbon', 'Teknisi ini sudah mencapai batas maksimal 2 kali kasbon rutin dalam minggu ini (Senin - Minggu).'); + } + } catch (\Exception $e) { + // Let built-in date validator handle formatting errors + } + } + } + }); + + if ($validator->fails()) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'errors' => $validator->errors(), + 'message' => $validator->errors()->first() + ], 422); + } + return redirect()->back()->withErrors($validator)->withInput(); + } + + try { + $data = $validator->validated(); + + // Map keterangan ke keperluan (database schema) + if (isset($data['keterangan'])) { + $data['keperluan'] = $data['keterangan']; + unset($data['keterangan']); + } + + // Set default status jika tidak ada + if (!isset($data['status'])) { + $data['status'] = Kasbon::STATUS_BELUM_LUNAS; + } + + $kasbon = Kasbon::create($data); + + if ($request->expectsJson()) { + return response()->json([ + 'success' => true, + 'data' => $kasbon, + 'message' => 'Kasbon berhasil ditambahkan' + ], 201); + } + + return redirect()->route('kasbon.index')->with('success', 'Kasbon berhasil ditambahkan'); + } catch (\Exception $e) { + \Illuminate\Support\Facades\Log::error('Kasbon Store Error', [ + 'message' => $e->getMessage(), + 'data' => $request->all(), + 'trace' => $e->getTraceAsString() + ]); + + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Terjadi kesalahan saat menyimpan data: ' . $e->getMessage() + ], 500); + } + + return redirect()->back()->with('error', 'Terjadi kesalahan saat menyimpan data: ' . $e->getMessage())->withInput(); + } + } + + /** + * Display the specified resource. + * + * @param int $id + * @param Request $request + * @return View|JsonResponse + */ + public function show(int $id, Request $request) + { + try { + $kasbon = Kasbon::findOrFail($id); + + if ($request->expectsJson()) { + return response()->json([ + 'success' => true, + 'data' => $kasbon, + 'message' => 'Data kasbon berhasil diambil' + ]); + } + + return view('Admin.Gaji.show-kasbon', compact('kasbon')); // DIPERBAIKI: path view + } catch (\Exception $e) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Kasbon tidak ditemukan' + ], 404); + } + + return redirect()->route('kasbon.index')->with('error', 'Kasbon tidak ditemukan'); + } + } + + /** + * Show the form for editing the specified resource. + * + * @param int $id + * @return View|RedirectResponse + */ + public function edit(int $id, Request $request) + { + try { + $kasbon = Kasbon::with('teknisi')->findOrFail($id); + $statusOptions = Kasbon::getStatusOptions(); + $teknisis = Teknisi::orderBy('nama')->get(); + + if ($request->expectsJson()) { + return response()->json([ + 'success' => true, + 'kasbon' => $kasbon, + 'statusOptions' => $statusOptions, + 'teknisis' => $teknisis + ]); + } + + return view('Admin.Gaji.edit-kasbon', compact('kasbon', 'statusOptions', 'teknisis')); + } catch (\Exception $e) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Kasbon tidak ditemukan' + ], 404); + } + return redirect()->route('kasbon.index')->with('error', 'Kasbon tidak ditemukan'); + } + } + + /** + * Update the specified resource in storage. + * + * @param Request $request + * @param int $id + * @return RedirectResponse|JsonResponse + */ + public function update(Request $request, int $id) + { + $validator = Validator::make($request->all(), [ + 'id_teknisi' => 'required|integer|min:1', + 'jumlah_kasbon' => 'required|numeric|min:0', + 'tanggal_kasbon' => 'required|date', + 'status' => 'nullable|in:lunas,belum_lunas', + 'keterangan' => 'nullable|string|max:100' + ], [ + 'id_teknisi.required' => 'ID Teknisi harus diisi', + 'id_teknisi.integer' => 'ID Teknisi harus berupa angka', + 'jumlah_kasbon.required' => 'Jumlah kasbon harus diisi', + 'jumlah_kasbon.numeric' => 'Jumlah kasbon harus berupa angka', + 'jumlah_kasbon.min' => 'Jumlah kasbon minimal 0', + 'tanggal_kasbon.required' => 'Tanggal kasbon harus diisi', + 'tanggal_kasbon.date' => 'Format tanggal kasbon tidak valid', + 'status.required' => 'Status harus dipilih', + 'status.in' => 'Status harus lunas atau belum_lunas', + 'keterangan.max' => 'Keterangan maksimal 500 karakter' + ]); + + $validator->after(function ($validator) use ($request, $id) { + $jumlah = (float) $request->input('jumlah_kasbon'); + $idTeknisi = $request->input('id_teknisi'); + $tanggal = $request->input('tanggal_kasbon'); + + if ($idTeknisi) { + $hasWork = Penugasan::where(function ($q) use ($idTeknisi) { + $q->where('id_teknisi', $idTeknisi) + ->orWhereHas('timTeknisi', function ($sq) use ($idTeknisi) { + $sq->where('id_teknisi', $idTeknisi) + ->where('status_kehadiran', 'hadir'); + }); + })->where('status_pekerjaan', 'selesai')->exists(); + + if (!$hasWork) { + $validator->errors()->add('id_teknisi', 'Teknisi belum menyelesaikan pekerjaan apapun. Harus menyelesaikan minimal satu pekerjaan sebelum kasbon.'); + } + } + + if ($jumlah > 0 && $jumlah <= 500000) { + // Aturan 1: Minimal Rp 200.000 untuk Kasbon Rutin + if ($jumlah < 200000) { + $validator->errors()->add('jumlah_kasbon', 'Jumlah kasbon rutin minimal Rp 200.000. Di atas Rp 500.000 dianggap pinjaman besar.'); + } + + // Aturan 2: Maksimal 2 kali kasbon rutin dalam 1 minggu kalender + if ($idTeknisi && $tanggal) { + try { + $date = Carbon::parse($tanggal); + $startOfWeek = $date->copy()->startOfWeek()->toDateString(); + $endOfWeek = $date->copy()->endOfWeek()->toDateString(); + + $kasbonCount = Kasbon::where('id_teknisi', $idTeknisi) + ->where('id_kasbon', '!=', $id) + ->where('jumlah_kasbon', '<=', 500000) + ->whereBetween('tanggal_kasbon', [$startOfWeek, $endOfWeek]) + ->count(); + + if ($kasbonCount >= 2) { + $validator->errors()->add('tanggal_kasbon', 'Teknisi ini sudah mencapai batas maksimal 2 kali kasbon rutin dalam minggu ini (Senin - Minggu).'); + } + } catch (\Exception $e) { + // Let built-in date validator handle formatting errors + } + } + } + }); + + if ($validator->fails()) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'errors' => $validator->errors(), + 'message' => $validator->errors()->first() + ], 422); + } + return redirect()->back()->withErrors($validator)->withInput(); + } + + try { + $kasbon = Kasbon::findOrFail($id); + $data = $validator->validated(); + + // Map keterangan ke keperluan + if (isset($data['keterangan'])) { + $data['keperluan'] = $data['keterangan']; + unset($data['keterangan']); + } + + $kasbon->update($data); + + if ($request->expectsJson()) { + return response()->json([ + 'success' => true, + 'data' => $kasbon, + 'message' => 'Kasbon berhasil diupdate' + ]); + } + + return redirect()->route('kasbon.index')->with('success', 'Kasbon berhasil diupdate'); + } catch (\Exception $e) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Kasbon tidak ditemukan atau terjadi kesalahan' + ], 404); + } + + return redirect()->route('kasbon.index')->with('error', 'Kasbon tidak ditemukan atau terjadi kesalahan'); + } + } + + /** + * Remove the specified resource from storage. + * + * @param Request $request + * @param int $id + * @return RedirectResponse|JsonResponse + */ + public function destroy(Request $request, int $id) + { + try { + $kasbon = Kasbon::findOrFail($id); + $kasbon->delete(); + + if ($request->expectsJson()) { + return response()->json([ + 'success' => true, + 'message' => 'Kasbon berhasil dihapus' + ]); + } + + return redirect()->route('kasbon.index')->with('success', 'Kasbon berhasil dihapus'); + } catch (\Exception $e) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Kasbon tidak ditemukan atau terjadi kesalahan' + ], 404); + } + + return redirect()->route('kasbon.index')->with('error', 'Kasbon tidak ditemukan atau terjadi kesalahan'); + } + } + + /** + * Get kasbon statistics + * + * @param Request $request + * @return JsonResponse + */ + public function statistics(Request $request): JsonResponse + { + try { + $totalKasbon = Kasbon::count(); + $totalJumlahKasbon = Kasbon::sum('jumlah_kasbon'); + $kasbonLunas = Kasbon::lunas()->count(); + $kasbonBelumLunas = Kasbon::belumLunas()->count(); + $totalJumlahLunas = Kasbon::lunas()->sum('jumlah_kasbon'); + $totalJumlahBelumLunas = Kasbon::belumLunas()->sum('jumlah_kasbon'); + + return response()->json([ + 'success' => true, + 'data' => [ + 'total_kasbon' => $totalKasbon, + 'total_jumlah_kasbon' => $totalJumlahKasbon, + 'kasbon_lunas' => $kasbonLunas, + 'kasbon_belum_lunas' => $kasbonBelumLunas, + 'total_jumlah_lunas' => $totalJumlahLunas, + 'total_jumlah_belum_lunas' => $totalJumlahBelumLunas, + ], + 'message' => 'Statistik kasbon berhasil diambil' + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Terjadi kesalahan saat mengambil statistik' + ], 500); + } + } + + /** + * Update status kasbon to lunas + * + * @param Request $request + * @param int $id + * @return RedirectResponse|JsonResponse + */ + public function markAsLunas(Request $request, int $id) + { + try { + $kasbon = Kasbon::findOrFail($id); + $kasbon->update(['status' => Kasbon::STATUS_LUNAS]); + + if ($request->expectsJson()) { + return response()->json([ + 'success' => true, + 'data' => $kasbon, + 'message' => 'Kasbon berhasil diubah menjadi lunas' + ]); + } + + return redirect()->back()->with('success', 'Kasbon berhasil diubah menjadi lunas'); + } catch (\Exception $e) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Kasbon tidak ditemukan' + ], 404); + } + + return redirect()->back()->with('error', 'Kasbon tidak ditemukan'); + } + } +} \ No newline at end of file diff --git a/samooapk/app/Http/Controllers/KelolaAdminController.php b/samooapk/app/Http/Controllers/KelolaAdminController.php new file mode 100644 index 0000000..89b11a3 --- /dev/null +++ b/samooapk/app/Http/Controllers/KelolaAdminController.php @@ -0,0 +1,174 @@ +get(); + return view('Admin.KelolaAdmin.index', compact('admins')); + } + + /** + * Simpan akun admin baru ke database. + */ + public function store(Request $request): JsonResponse + { + $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:users,email'], + 'password' => ['required', 'string', 'min:8'], + ], [ + 'name.required' => 'Nama wajib diisi.', + 'email.required' => 'Email wajib diisi.', + 'email.email' => 'Format email tidak valid.', + 'email.unique' => 'Email sudah terdaftar.', + 'password.required' => 'Password wajib diisi.', + 'password.min' => 'Password minimal 8 karakter.', + ]); + + try { + $admin = User::create([ + 'name' => $request->name, + 'email' => strtolower($request->email), + 'password' => Hash::make($request->password), + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Akun admin berhasil dibuat!', + 'data' => $admin, + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal membuat akun admin: ' . $e->getMessage(), + ], 500); + } + } + + /** + * Tampilkan detail akun admin tertentu (JSON). + */ + public function show(string $id): JsonResponse + { + try { + $admin = User::findOrFail($id); + return response()->json([ + 'success' => true, + 'data' => $admin, + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Akun admin tidak ditemukan.', + ], 404); + } + } + + /** + * Ambil data akun admin untuk form edit (JSON). + */ + public function edit(string $id): JsonResponse + { + try { + $admin = User::findOrFail($id); + return response()->json($admin); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Akun admin tidak ditemukan.', + ], 404); + } + } + + /** + * Update akun admin di database. + */ + public function update(Request $request, string $id): JsonResponse + { + try { + $admin = User::findOrFail($id); + + $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', "unique:users,email,{$id}"], + 'password' => ['nullable', 'string', 'min:8'], + ], [ + 'name.required' => 'Nama wajib diisi.', + 'email.required' => 'Email wajib diisi.', + 'email.email' => 'Format email tidak valid.', + 'email.unique' => 'Email sudah digunakan oleh akun lain.', + 'password.min' => 'Password minimal 8 karakter.', + ]); + + $updateData = [ + 'name' => $request->name, + 'email' => strtolower($request->email), + ]; + + if ($request->filled('password')) { + $updateData['password'] = Hash::make($request->password); + } + + $admin->update($updateData); + + return response()->json([ + 'success' => true, + 'message' => 'Akun admin berhasil diperbarui!', + ]); + } catch (\Illuminate\Validation\ValidationException $e) { + return response()->json([ + 'success' => false, + 'errors' => $e->errors(), + ], 422); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal memperbarui akun admin: ' . $e->getMessage(), + ], 500); + } + } + + /** + * Hapus akun admin dari database. + */ + public function destroy(string $id): JsonResponse + { + try { + // Tidak boleh menghapus akun sendiri + if ((string) Auth::id() === $id) { + return response()->json([ + 'success' => false, + 'message' => 'Anda tidak dapat menghapus akun Anda sendiri.', + ], 403); + } + + $admin = User::findOrFail($id); + $admin->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Akun admin berhasil dihapus!', + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal menghapus akun admin: ' . $e->getMessage(), + ], 500); + } + } +} diff --git a/samooapk/app/Http/Controllers/LaporanController.php b/samooapk/app/Http/Controllers/LaporanController.php new file mode 100644 index 0000000..b9fedc6 --- /dev/null +++ b/samooapk/app/Http/Controllers/LaporanController.php @@ -0,0 +1,339 @@ +leftJoin('teknisis', 'kasbons.id_teknisi', '=', 'teknisis.id_teknisi') + ->select('kasbons.*', 'teknisis.nama as nama_teknisi') + ->orderBy('kasbons.created_at', 'desc') + ->limit(5)->get(); + + $recentAbsensi = DB::table('absensis') + ->leftJoin('teknisis', 'absensis.id_teknisi', '=', 'teknisis.id_teknisi') + ->select('absensis.*', 'teknisis.nama as nama_teknisi') + ->orderBy('absensis.tanggal', 'desc') + ->limit(5)->get(); + + $recentPekerjaan = DB::table('penugasans') + ->leftJoin('teknisis', 'penugasans.id_teknisi', '=', 'teknisis.id_teknisi') + ->select('penugasans.*', 'teknisis.nama as nama_teknisi') + ->orderBy('penugasans.created_at', 'desc') + ->limit(5)->get(); + + return view('Admin.Laporan', compact( + 'statistics', 'recentKasbon', 'recentAbsensi', 'recentPekerjaan' + )); + } catch (Exception $e) { + \Log::error('Laporan Error: ' . $e->getMessage()); + return back()->with('error', 'Gagal memuat laporan: ' . $e->getMessage()); + } + } + + /** + * Get statistics for AJAX refresh + */ + public function statistics() + { + return response()->json([ + 'success' => true, + 'data' => Laporan::getStatistics() + ]); + } + + /** + * Detailed Kasbon Report + */ + public function kasbon(Request $request) + { + $filters = $request->only(['tanggal_dari', 'tanggal_sampai', 'status', 'id_teknisi', 'search']); + $data = Laporan::getKasbonData($filters)->paginate(15)->appends($filters); + $teknisis = Teknisi::orderBy('nama')->get(); + + // Statistik ringkas kasbon + $statsKasbon = [ + 'total' => DB::table('kasbons')->count(), + 'lunas' => DB::table('kasbons')->where('status', 'lunas')->count(), + 'belum_lunas' => DB::table('kasbons')->where('status', 'belum_lunas')->count(), + 'total_nominal' => DB::table('kasbons')->sum('jumlah_kasbon'), + 'total_belum_lunas' => DB::table('kasbons')->where('status', 'belum_lunas')->sum('jumlah_kasbon'), + ]; + + return view('Admin.Laporan.kasbon', compact('data', 'teknisis', 'filters', 'statsKasbon')); + } + + /** + * Detailed Teknisi/Kehadiran Report + */ + public function teknisi(Request $request) + { + $filters = $request->only(['tanggal_dari', 'tanggal_sampai', 'search']); + $data = Laporan::getTeknisiData($filters)->get(); + return view('Admin.Laporan.teknisi', compact('data', 'filters')); + } + + /** + * Detailed Absensi Report + */ + public function absensi(Request $request) + { + $filters = $request->only(['tanggal_dari', 'tanggal_sampai', 'status', 'id_teknisi', 'search', 'tanggal']); + if (!empty($filters['tanggal'])) { + $filters['tanggal_dari'] = $filters['tanggal']; + $filters['tanggal_sampai'] = $filters['tanggal']; + } + $data = Laporan::getAbsensiData($filters)->paginate(15)->appends($filters); + $teknisis = Teknisi::orderBy('nama')->get(); + + // Statistik absensi bulan ini + $statsAbsensi = [ + 'hadir' => DB::table('absensis')->where('status', 'hadir')->whereMonth('tanggal', date('m'))->whereYear('tanggal', date('Y'))->count(), + 'izin' => DB::table('absensis')->where('status', 'izin')->whereMonth('tanggal', date('m'))->whereYear('tanggal', date('Y'))->count(), + 'sakit' => DB::table('absensis')->where('status', 'sakit')->whereMonth('tanggal', date('m'))->whereYear('tanggal', date('Y'))->count(), + 'alpha' => DB::table('absensis')->where('status', 'alpha')->whereMonth('tanggal', date('m'))->whereYear('tanggal', date('Y'))->count(), + ]; + + return view('Admin.Laporan.absensi', compact('data', 'teknisis', 'filters', 'statsAbsensi')); + } + + /** + * Detailed Pekerjaan Report + */ + public function pekerjaan(Request $request) + { + $filters = $request->only(['tanggal_dari', 'tanggal_sampai', 'status', 'id_teknisi', 'search', 'tanggal']); + if (!empty($filters['tanggal'])) { + $filters['tanggal_dari'] = $filters['tanggal']; + $filters['tanggal_sampai'] = $filters['tanggal']; + } + $data = Laporan::getPekerjaanData($filters)->paginate(15)->appends($filters); + $teknisis = Teknisi::orderBy('nama')->get(); + + $statsPekerjaan = [ + 'total' => DB::table('penugasans')->count(), + 'selesai' => DB::table('penugasans')->where('status_pekerjaan', 'selesai')->count(), + 'proses' => DB::table('penugasans')->where('status_pekerjaan', 'proses')->count(), + 'pending' => DB::table('penugasans')->where('status_pekerjaan', 'pending')->count(), + ]; + + return view('Admin.Laporan.pekerjaan', compact('data', 'teknisis', 'filters', 'statsPekerjaan')); + } + + // ============================================================ + // LAPORAN PENGGAJIAN & DATA TEKNISI + // ============================================================ + + public function penggajian(Request $request) + { + $filters = $request->only(['tanggal_dari', 'tanggal_sampai', 'status', 'id_teknisi', 'search', 'tanggal']); + if (!empty($filters['tanggal'])) { + $filters['tanggal_dari'] = $filters['tanggal']; + $filters['tanggal_sampai'] = $filters['tanggal']; + } + + $query = DB::table('penggajians') + ->leftJoin('teknisis', 'penggajians.id_teknisi', '=', 'teknisis.id_teknisi') + ->select('penggajians.*', 'teknisis.nama as nama_teknisi') + ->orderBy('penggajians.created_at', 'desc'); + + if (!empty($filters['id_teknisi'])) { + $query->where('penggajians.id_teknisi', $filters['id_teknisi']); + } + if (!empty($filters['status'])) { + $query->where('penggajians.status_pembayaran', $filters['status']); + } + if (!empty($filters['tanggal_dari'])) { + $query->whereDate('penggajians.created_at', '>=', $filters['tanggal_dari']); + } + if (!empty($filters['tanggal_sampai'])) { + $query->whereDate('penggajians.created_at', '<=', $filters['tanggal_sampai']); + } + + $data = $query->paginate(15)->appends($filters); + $teknisis = Teknisi::orderBy('nama')->get(); + + $statsPenggajian = [ + 'total' => DB::table('penggajians')->count(), + 'lunas' => DB::table('penggajians')->where('status_pembayaran', 'lunas')->count(), + 'belum' => DB::table('penggajians')->where('status_pembayaran', 'belum_bayar')->count(), + ]; + + return view('Admin.Laporan.penggajian', compact('data', 'teknisis', 'filters', 'statsPenggajian')); + } + + public function dataTeknisi(Request $request) + { + $filters = $request->only(['status', 'search']); + $query = Teknisi::query(); + if (!empty($filters['search'])) { + $query->where('nama', 'like', '%'.$filters['search'].'%'); + } + if (!empty($filters['status'])) { + $query->where('status', $filters['status']); + } + $data = $query->paginate(15)->appends($filters); + + $statsTeknisi = [ + 'total' => Teknisi::count(), + 'aktif' => Teknisi::where('status', 'aktif')->count(), + 'nonaktif' => Teknisi::where('status', 'nonaktif')->count(), + ]; + + return view('Admin.Laporan.data_teknisi', compact('data', 'filters', 'statsTeknisi')); + } + + // ============================================================ + // CETAK LAPORAN (PRINT) METHODS + // ============================================================ + + /** + * Cetak Laporan Kasbon + */ + public function exportKasbon(Request $request) + { + $filters = $request->only(['tanggal_dari', 'tanggal_sampai', 'status', 'id_teknisi', 'search']); + $data = Laporan::getKasbonData($filters)->get(); + $title = 'Kasbon Teknisi'; + $type = 'kasbon'; + $columns = ['Nama Teknisi', 'Tanggal Pinjam', 'Jumlah Kasbon', 'Status', 'Keperluan']; + + return view('Admin.Laporan.print', compact('data', 'filters', 'title', 'type', 'columns')); + } + + /** + * Cetak Laporan Teknisi/Kehadiran + */ + public function exportTeknisi(Request $request) + { + $filters = $request->only(['tanggal_dari', 'tanggal_sampai', 'search']); + $data = Laporan::getTeknisiData($filters)->get(); + $title = 'Performa & Kehadiran Teknisi'; + $type = 'teknisi'; + $columns = ['Nama Teknisi', 'Status', 'Hadir', 'Izin', 'Sakit', 'Alpha', 'Total Hari', 'Persentase']; + + return view('Admin.Laporan.print', compact('data', 'filters', 'title', 'type', 'columns')); + } + + /** + * Cetak Laporan Absensi + */ + public function exportAbsensi(Request $request) + { + $filters = $request->only(['tanggal_dari', 'tanggal_sampai', 'status', 'id_teknisi', 'search']); + $data = Laporan::getAbsensiData($filters)->get(); + $title = 'Absensi Harian'; + $type = 'absensi'; + $columns = ['Nama Teknisi', 'Tanggal', 'Status Kehadiran', 'Keterangan']; + + return view('Admin.Laporan.print', compact('data', 'filters', 'title', 'type', 'columns')); + } + + /** + * Cetak Laporan Pekerjaan + */ + public function exportPekerjaan(Request $request) + { + $filters = $request->only(['tanggal_dari', 'tanggal_sampai', 'status', 'id_teknisi', 'search']); + $data = Laporan::getPekerjaanData($filters)->get(); + $title = 'Pekerjaan Teknisi'; + $type = 'pekerjaan'; + $columns = ['ID', 'Jenis Pekerjaan', 'Nama Teknisi', 'Status', 'Tgl Mulai', 'Tgl Selesai']; + + return view('Admin.Laporan.print', compact('data', 'filters', 'title', 'type', 'columns')); + } + + /** + * Cetak Laporan Penggajian + */ + public function exportPenggajian(Request $request) + { + $filters = $request->only(['tanggal_dari', 'tanggal_sampai', 'status', 'id_teknisi', 'search', 'tanggal']); + if (!empty($filters['tanggal'])) { + $filters['tanggal_dari'] = $filters['tanggal']; + $filters['tanggal_sampai'] = $filters['tanggal']; + } + + $query = DB::table('penggajians') + ->leftJoin('teknisis', 'penggajians.id_teknisi', '=', 'teknisis.id_teknisi') + ->select('penggajians.*', 'teknisis.nama as nama_teknisi') + ->orderBy('penggajians.created_at', 'desc'); + + if (!empty($filters['id_teknisi'])) { + $query->where('penggajians.id_teknisi', $filters['id_teknisi']); + } + if (!empty($filters['status'])) { + $query->where('penggajians.status_pembayaran', $filters['status']); + } + if (!empty($filters['tanggal_dari'])) { + $query->whereDate('penggajians.created_at', '>=', $filters['tanggal_dari']); + } + if (!empty($filters['tanggal_sampai'])) { + $query->whereDate('penggajians.created_at', '<=', $filters['tanggal_sampai']); + } + + $data = $query->get(); + $title = 'Laporan Penggajian'; + $type = 'penggajian'; + $columns = ['Nama Teknisi', 'Periode', 'Total Gaji', 'Status Pembayaran']; + + return view('Admin.Laporan.print', compact('data', 'filters', 'title', 'type', 'columns')); + } + + /** + * Cetak Laporan Data Teknisi + */ + public function exportDataTeknisi(Request $request) + { + $filters = $request->only(['status', 'search']); + $query = Teknisi::query(); + if (!empty($filters['search'])) { + $query->where('nama', 'like', '%'.$filters['search'].'%'); + } + if (!empty($filters['status'])) { + $query->where('status', $filters['status']); + } + + $data = $query->get(); + $title = 'Data Teknisi'; + $type = 'data_teknisi'; + $columns = ['Nama Teknisi', 'Email', 'No Telepon', 'Tanggal Masuk', 'Status']; + + return view('Admin.Laporan.print', compact('data', 'filters', 'title', 'type', 'columns')); + } + + /** + * Generic export (from Ringkasan page) + */ + public function export(Request $request) + { + $jenis = $request->get('jenis_laporan', 'kasbon'); + + switch ($jenis) { + case 'kasbon': return $this->exportKasbon($request); + case 'teknisi': return $this->exportTeknisi($request); + case 'absensi': return $this->exportAbsensi($request); + case 'pekerjaan': return $this->exportPekerjaan($request); + case 'penggajian': return $this->exportPenggajian($request); + case 'data_teknisi': return $this->exportDataTeknisi($request); + default: return back()->with('error', 'Jenis laporan tidak valid.'); + } + } +} \ No newline at end of file diff --git a/samooapk/app/Http/Controllers/PenggajianController.php b/samooapk/app/Http/Controllers/PenggajianController.php new file mode 100644 index 0000000..4a718e1 --- /dev/null +++ b/samooapk/app/Http/Controllers/PenggajianController.php @@ -0,0 +1,907 @@ +latest('periode_tahun') + ->latest('periode_bulan'); + + if ($request->filled('periode_bulan')) { + $query->where('periode_bulan', $request->periode_bulan); + } + if ($request->filled('periode_tahun')) { + $query->where('periode_tahun', $request->periode_tahun); + } + if ($request->filled('status_pembayaran')) { + $query->where('status_pembayaran', $request->status_pembayaran); + } + if ($request->filled('id_teknisi')) { + $query->where('id_teknisi', $request->id_teknisi); + } + + if ($request->expectsJson()) { + $penggajians = $query->get(); + $rows = $penggajians->map(function ($g) { + return [ + 'id_penggajian' => $g->id_penggajian, + 'nama_teknisi' => $g->teknisi->nama ?? 'N/A', + 'periode_label' => Carbon::create()->month($g->periode_bulan)->format('M') . ' ' . $g->periode_tahun, + 'tanggal_penggajian' => $g->tanggal_penggajian->format('d/m/Y'), + 'jumlah_hari_kerja' => $g->jumlah_hari_kerja, + 'total_ongkos_pekerjaan' => $g->total_ongkos_pekerjaan, + 'biaya_makan' => $g->biaya_makan, + 'total_kasbon' => $g->total_kasbon, + 'gaji_bersih' => $g->gaji_bersih, + 'status_pembayaran' => $g->status_pembayaran, + ]; + }); + $summary = $penggajians->count() > 0 ? [ + 'total_teknisi' => $penggajians->count(), + 'total_gaji' => $penggajians->sum('total_ongkos_pekerjaan'), + 'total_kasbon' => $penggajians->sum('total_kasbon') + $penggajians->sum('biaya_makan'), + 'gaji_bersih' => $penggajians->sum('gaji_bersih'), + ] : null; + return response()->json(['rows' => $rows, 'summary' => $summary]); + } + + $teknisiList = Teknisi::where('status', 'aktif')->orderBy('nama')->get(); + return view('Admin.Gaji.Penggajian', compact('teknisiList')); + } + + /** + * Show the form for creating a new resource. + */ + public function create() + { + $teknisis = Teknisi::where('status', 'aktif')->get(); + $tarifs = TarifPekerjaan::where('is_active', true)->get(); + return view('admin.penggajian.create', compact('teknisis', 'tarifs')); + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'id_teknisi' => 'required|exists:teknisis,id_teknisi', + 'periode_bulan' => 'required|integer|between:1,12', + 'periode_tahun' => 'required|integer|min:2020|max:' . (date('Y') + 1), + 'tanggal_penggajian' => 'required|date', + 'biaya_makan' => 'nullable|numeric|min:0', + 'total_kasbon' => 'nullable|numeric|min:0', + 'details' => 'required|array|min:1', + 'details.*.id_tarif' => 'required|exists:tarif_pekerjaans,id_tarif', + 'details.*.jumlah_unit' => 'required|integer|min:1', + 'details.*.tarif_per_unit' => 'required|numeric|min:0', + ]); + + if ($validator->fails()) { + return back()->withErrors($validator)->withInput(); + } + + $exists = Penggajian::isPeriodeExists( + $request->id_teknisi, + $request->periode_bulan, + $request->periode_tahun + ); + + if ($exists) { + return back()->withErrors([ + 'periode' => 'Penggajian untuk teknisi ini pada periode tersebut sudah ada.' + ])->withInput(); + } + + DB::beginTransaction(); + try { + $gaji_kotor = 0; + foreach ($request->details as $detail) { + $gaji_kotor += $detail['jumlah_unit'] * $detail['tarif_per_unit']; + } + + $penggajian = Penggajian::create([ + 'id_teknisi' => $request->id_teknisi, + 'periode_bulan' => $request->periode_bulan, + 'periode_tahun' => $request->periode_tahun, + 'tanggal_penggajian' => $request->tanggal_penggajian, + 'jumlah_hari_kerja' => $this->calculateHariKerja( + $request->id_teknisi, + $request->periode_bulan, + $request->periode_tahun + ), + 'gaji_kotor' => $gaji_kotor, + 'biaya_makan' => $request->biaya_makan ?? 0, + 'total_kasbon' => $request->total_kasbon ?? 0, + 'total_potongan' => $request->total_kasbon ?? 0, + 'status_pembayaran' => Penggajian::STATUS_BELUM_BAYAR, + ]); + + foreach ($request->details as $detail) { + DetailPenggajian::create([ + 'id_penggajian' => $penggajian->id_penggajian, + 'id_teknisi' => $request->id_teknisi, + 'id_tarif' => $detail['id_tarif'], + 'jumlah_unit' => $detail['jumlah_unit'], + 'tarif_per_unit' => $detail['tarif_per_unit'], + ]); + } + + DB::commit(); + return redirect()->route('penggajian.index') + ->with('success', 'Data penggajian berhasil dibuat.'); + + } catch (\Exception $e) { + DB::rollback(); + return back()->withErrors(['error' => 'Gagal menyimpan data penggajian.'])->withInput(); + } + } + + /** + * Display the specified resource. + */ + public function show(Penggajian $penggajian) + { + $penggajian->load(['teknisi', 'detailPenggajian.penugasan']); + return view('admin.penggajian.show', compact('penggajian')); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(Penggajian $penggajian) + { + $penggajian->load(['teknisi', 'detailPenggajian.penugasan']); + $teknisis = Teknisi::where('status', 'aktif')->get(); + $tarifs = TarifPekerjaan::where('is_active', true)->get(); + return view('admin.penggajian.edit', compact('penggajian', 'teknisis', 'tarifs')); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, Penggajian $penggajian) + { + $validator = Validator::make($request->all(), [ + 'tanggal_penggajian' => 'required|date', + 'biaya_makan' => 'nullable|numeric|min:0', + 'total_kasbon' => 'nullable|numeric|min:0', + 'details' => 'required|array|min:1', + 'details.*.id_tarif' => 'required|exists:tarif_pekerjaans,id_tarif', + 'details.*.jumlah_unit' => 'required|integer|min:1', + 'details.*.tarif_per_unit' => 'required|numeric|min:0', + ]); + + if ($validator->fails()) { + return back()->withErrors($validator)->withInput(); + } + + DB::beginTransaction(); + try { + $gaji_kotor = 0; + foreach ($request->details as $detail) { + $gaji_kotor += $detail['jumlah_unit'] * $detail['tarif_per_unit']; + } + + $penggajian->update([ + 'tanggal_penggajian' => $request->tanggal_penggajian, + 'total_ongkos_pekerjaan' => $gaji_kotor, + 'biaya_makan' => $request->biaya_makan ?? 0, + 'total_kasbon' => $request->total_kasbon ?? 0, + 'total_potongan' => $request->total_kasbon ?? 0, + ]); + + $penggajian->detailPenggajian()->delete(); + + $gajiData = $this->calculateGajiTeknisi( + $penggajian->id_teknisi, + $penggajian->periode_bulan, + $penggajian->periode_tahun, + $penggajian->tanggal_penggajian + ); + + foreach ($gajiData['details'] as $detail) { + DetailPenggajian::create([ + 'id_penggajian' => $penggajian->id_penggajian, + 'id_penugasan' => $detail['id_penugasan'], + 'tanggal_selesai' => $detail['tanggal_selesai'], + 'lokasi' => $detail['lokasi'], + 'ongkos_penugasan' => $detail['ongkos_penugasan'], + 'jumlah_tim' => $detail['jumlah_tim'], + 'bagian_ongkos' => $detail['bagian_ongkos'], + ]); + } + + DB::commit(); + return redirect()->route('penggajian.index') + ->with('success', 'Data penggajian berhasil diperbarui.'); + + } catch (\Exception $e) { + DB::rollback(); + return back()->withErrors(['error' => 'Gagal memperbarui data penggajian.'])->withInput(); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Penggajian $penggajian) + { + try { + DB::beginTransaction(); + $penggajian->detailPenggajian()->delete(); + $penggajian->delete(); + DB::commit(); + + return response()->json(['success' => true, 'message' => 'Data penggajian berhasil dihapus.']); + } catch (\Exception $e) { + DB::rollback(); + return response()->json(['success' => false, 'message' => 'Gagal menghapus data penggajian.'], 500); + } + } + + /** + * Hitung gaji untuk periode tertentu. + * + * Tambahan parameter: + * force_recalculate (boolean, default false) + * → Jika true, data penggajian yang sudah ada akan dihapus dan dihitung ulang. + * Gunakan ini ketika ada penugasan baru yang selesai SETELAH gaji di-generate, + * sehingga semua anggota tim (termasuk yang tidak jadi teknisi utama) ikut terhitung. + * → Jika false (default), teknisi yang sudah punya data di periode itu akan di-skip. + * + * CATATAN: Penggajian yang sudah berstatus 'sudah_bayar' TIDAK akan di-recalculate + * meski force_recalculate = true, untuk mencegah perubahan data yang sudah dibayar. + */ + public function hitungGaji(Request $request) + { + $validator = Validator::make($request->all(), [ + 'id_teknisi' => 'nullable|exists:teknisis,id_teknisi', + 'periode_bulan' => 'required|integer|between:1,12', + 'periode_tahun' => 'required|integer|min:2020|max:' . (date('Y') + 1), + 'tanggal_penggajian' => 'required|date', + 'include_kasbon' => 'boolean', + 'force_recalculate' => 'boolean', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Data tidak valid. ' . implode(' ', $validator->errors()->all()), + 'errors' => $validator->errors() + ], 422); + } + + try { + DB::beginTransaction(); + + $periode_bulan = $request->periode_bulan; + $periode_tahun = $request->periode_tahun; + $tanggal_penggajian = $request->tanggal_penggajian + ?? Carbon::create($periode_tahun, $periode_bulan)->endOfMonth()->format('Y-m-d'); + $include_kasbon = $request->boolean('include_kasbon', true); + $force_recalculate = $request->boolean('force_recalculate', false); + $id_teknisi = $request->id_teknisi; + + if ($id_teknisi) { + $teknisis = Teknisi::where('id_teknisi', $id_teknisi)->where('status', 'aktif')->get(); + } else { + $teknisis = Teknisi::where('status', 'aktif')->get(); + } + + $created_count = 0; + $skipped_count = 0; + $recalculated_count = 0; + + foreach ($teknisis as $teknisi) { + // Cek apakah sudah ada penggajian untuk periode ini + $existingPenggajian = Penggajian::where('id_teknisi', $teknisi->id_teknisi) + ->where('periode_bulan', $periode_bulan) + ->where('periode_tahun', $periode_tahun) + ->first(); + + if ($existingPenggajian) { + if (!$force_recalculate) { + // Tidak force → skip seperti perilaku lama + $skipped_count++; + continue; + } + + // Sudah dibayar → jangan recalculate, lindungi data yang sudah final + if ($existingPenggajian->status_pembayaran === Penggajian::STATUS_SUDAH_BAYAR) { + $skipped_count++; + continue; + } + + // Force recalculate → hapus detail dan penggajian lama, hitung ulang + $existingPenggajian->detailPenggajian()->delete(); + $existingPenggajian->delete(); + $recalculated_count++; + } + + $gajiData = $this->calculateGajiTeknisi( + $teknisi->id_teknisi, + $periode_bulan, + $periode_tahun, + $tanggal_penggajian, + $include_kasbon + ); + + if ($gajiData['gaji_kotor'] > 0 || $gajiData['biaya_makan'] > 0 || !empty($gajiData['details'])) { + $total_penugasan = !empty($gajiData['details']) ? count($gajiData['details']) : 0; + + $penggajian = Penggajian::create([ + 'id_teknisi' => $teknisi->id_teknisi, + 'periode_bulan' => $periode_bulan, + 'periode_tahun' => $periode_tahun, + 'tanggal_penggajian' => $tanggal_penggajian, + 'jumlah_hari_kerja' => $gajiData['jumlah_hari_kerja'], + 'jumlah_penugasan_selesai' => $total_penugasan, + 'total_ongkos_pekerjaan' => $gajiData['gaji_kotor'], + 'biaya_makan' => $gajiData['biaya_makan'], + 'total_kasbon' => $gajiData['total_kasbon'], + 'total_potongan' => $gajiData['total_kasbon'], + 'gaji_bersih' => $gajiData['gaji_kotor'] - $gajiData['biaya_makan'] - $gajiData['total_kasbon'], + 'status_pembayaran' => Penggajian::STATUS_BELUM_BAYAR, + ]); + + foreach ($gajiData['details'] as $detail) { + DetailPenggajian::create([ + 'id_penggajian' => $penggajian->id_penggajian, + 'id_penugasan' => $detail['id_penugasan'], + 'tanggal_selesai' => $detail['tanggal_selesai'], + 'lokasi' => $detail['lokasi'], + 'ongkos_penugasan' => $detail['ongkos_penugasan'], + 'jumlah_tim' => $detail['jumlah_tim'], + 'bagian_ongkos' => $detail['bagian_ongkos'], + 'rincian_pekerjaan'=> $detail['rincian_pekerjaan'] ?? null, + ]); + } + + $created_count++; + } + } + + DB::commit(); + + $message = "Berhasil menghitung gaji untuk {$created_count} teknisi."; + if ($recalculated_count > 0) { + $message .= " {$recalculated_count} teknisi dihitung ulang."; + } + if ($skipped_count > 0) { + $message .= " {$skipped_count} teknisi dilewati (sudah ada data atau sudah dibayar)."; + } + + return response()->json([ + 'success' => true, + 'message' => $message, + 'data' => [ + 'created' => $created_count, + 'recalculated' => $recalculated_count, + 'skipped' => $skipped_count, + ] + ]); + + } catch (\Exception $e) { + DB::rollback(); + return response()->json([ + 'success' => false, + 'message' => 'Gagal menghitung gaji: ' . $e->getMessage() + ], 500); + } + } + + /** + * Hitung ulang penggajian untuk satu teknisi tertentu. + * + * Endpoint: POST /penggajian/{penggajian}/recalculate + * + * Berguna ketika: + * - Ada penugasan tim yang baru selesai setelah gaji di-generate + * - Anggota tim yang tidak jadi teknisi utama tidak muncul di rincian + * - Data detail penggajian tidak lengkap / tidak sesuai + * + * Penggajian yang sudah 'sudah_bayar' tidak bisa di-recalculate. + */ + public function recalculate(Penggajian $penggajian) + { + try { + DB::beginTransaction(); + + // Hapus detail lama + $penggajian->detailPenggajian()->delete(); + + // Jika sudah lunas, kita tidak ingin menarik data kasbon baru dari database + // karena kasbon aslinya mungkin sudah berstatus lunas. + // Kita gunakan nilai total_kasbon yang sudah tersimpan di record ini. + $isPaid = $penggajian->status_pembayaran === Penggajian::STATUS_SUDAH_BAYAR; + + $gajiData = $this->calculateGajiTeknisi( + $penggajian->id_teknisi, + $penggajian->periode_bulan, + $penggajian->periode_tahun, + $penggajian->tanggal_penggajian, + !$isPaid // includeKasbon hanya jika belum bayar + ); + + $total_penugasan = count($gajiData['details']); + + // Update header penggajian + $penggajian->update([ + 'jumlah_hari_kerja' => $gajiData['jumlah_hari_kerja'], + 'jumlah_penugasan_selesai' => $total_penugasan, + 'total_ongkos_pekerjaan' => $gajiData['gaji_kotor'], + 'biaya_makan' => $gajiData['biaya_makan'], + 'total_kasbon' => $isPaid ? $penggajian->total_kasbon : $gajiData['total_kasbon'], + 'total_potongan' => $isPaid ? $penggajian->total_kasbon : $gajiData['total_kasbon'], + 'gaji_bersih' => $gajiData['gaji_kotor'] - $gajiData['biaya_makan'] - ($isPaid ? $penggajian->total_kasbon : $gajiData['total_kasbon']), + ]); + + // Insert detail baru + foreach ($gajiData['details'] as $detail) { + DetailPenggajian::create([ + 'id_penggajian' => $penggajian->id_penggajian, + 'id_penugasan' => $detail['id_penugasan'], + 'tanggal_selesai' => $detail['tanggal_selesai'], + 'lokasi' => $detail['lokasi'], + 'ongkos_penugasan' => $detail['ongkos_penugasan'], + 'jumlah_tim' => $detail['jumlah_tim'], + 'bagian_ongkos' => $detail['bagian_ongkos'], + 'rincian_pekerjaan'=> $detail['rincian_pekerjaan'] ?? null, + ]); + } + + DB::commit(); + + return response()->json([ + 'success' => true, + 'message' => "Penggajian berhasil dihitung ulang. Ditemukan {$total_penugasan} penugasan.", + 'data' => [ + 'total_penugasan' => $total_penugasan, + 'total_ongkos' => $gajiData['gaji_kotor'], + 'jumlah_hari_kerja' => $gajiData['jumlah_hari_kerja'], + 'biaya_makan' => $gajiData['biaya_makan'], + 'total_kasbon' => $gajiData['total_kasbon'], + ] + ]); + + } catch (\Exception $e) { + DB::rollback(); + return response()->json([ + 'success' => false, + 'message' => 'Gagal menghitung ulang: ' . $e->getMessage() + ], 500); + } + } + + /** + * Get detail penggajian for modal + */ + public function detail(Penggajian $penggajian) + { + $penggajian->load(['teknisi', 'detailPenggajian.penugasan']); + return view('Admin.Gaji.detail_penggajian', compact('penggajian')); + } + + /** + * Process payment for specific penggajian + */ + public function prosesPembayaran(Penggajian $penggajian) + { + try { + if ($penggajian->isPaid()) { + return response()->json(['success' => false, 'message' => 'Gaji sudah dibayar sebelumnya.']); + } + + $penggajian->markAsPaid(); + + // ── LOGIKA PELUNASAN KASBON (DENGAN SISTEM CICILAN/PARTIAL) ── + $sisaPotongan = (float) $penggajian->total_kasbon; + + if ($sisaPotongan > 0) { + // Ambil semua kasbon belum lunas, urutkan dari yang paling lama + $kasbons = Kasbon::where('id_teknisi', $penggajian->id_teknisi) + ->where('status', 'belum_lunas') + ->whereDate('tanggal_kasbon', '<=', $penggajian->tanggal_penggajian) + ->orderBy('tanggal_kasbon', 'asc') + ->get(); + + foreach ($kasbons as $kb) { + if ($sisaPotongan <= 0) break; + + $jumlahKb = (float) $kb->jumlah_kasbon; + + if ($sisaPotongan >= $jumlahKb) { + // Kasbon ini terbayar penuh + $kb->update(['status' => 'lunas']); + $sisaPotongan -= $jumlahKb; + } else { + // Kasbon ini terbayar sebagian (CICILAN) + // 1. Kurangi nilai kasbon lama + $kb->update([ + 'jumlah_kasbon' => $jumlahKb - $sisaPotongan + ]); + + // 2. Buat record baru untuk bagian yang sudah lunas (untuk histori) + $newLunas = $kb->replicate(); + $newLunas->jumlah_kasbon = $sisaPotongan; + $newLunas->status = 'lunas'; + $newLunas->keperluan = $kb->keperluan . ' (Cicilan via Gaji ' . $penggajian->formatted_periode . ')'; + $newLunas->save(); + + $sisaPotongan = 0; + } + } + } + + return response()->json([ + 'success' => true, + 'message' => 'Pembayaran berhasil diproses dan kasbon telah dilunasi.' + ]); + + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => 'Gagal memproses pembayaran.'], 500); + } + } + + public function updateKasbon(Request $request, Penggajian $penggajian) + { + try { + $rawInput = $request->input('total_kasbon', 0); + $cleanInput = preg_replace('/[^0-9]/', '', $rawInput); + $newKasbon = (float) $cleanInput; + + if ($newKasbon < 0) { + return response()->json(['success' => false, 'message' => 'Nominal kasbon tidak boleh negatif.'], 422); + } + + $penggajian->update([ + 'total_kasbon' => $newKasbon, + 'total_potongan' => $newKasbon, + 'gaji_bersih' => $penggajian->total_ongkos_pekerjaan - $penggajian->biaya_makan - $newKasbon, + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Potongan kasbon berhasil diubah menjadi Rp ' . number_format($newKasbon, 0, ',', '.') . '. Gaji bersih akan dihitung ulang otomatis.' + ]); + + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => 'Gagal mengubah potongan: ' . $e->getMessage()], 500); + } + } + + /** + * Update food deduction amount manually. + * Allows mandor to waive food costs for bereavement or emergencies. + */ + public function updateMakan(Request $request, Penggajian $penggajian) + { + try { + $rawInput = $request->input('biaya_makan', 0); + $cleanInput = preg_replace('/[^0-9]/', '', $rawInput); + $newMakan = (float) $cleanInput; + + if ($newMakan < 0) { + return response()->json(['success' => false, 'message' => 'Nominal biaya makan tidak boleh negatif.'], 422); + } + + $penggajian->update([ + 'biaya_makan' => $newMakan, + 'gaji_bersih' => $penggajian->total_ongkos_pekerjaan - $newMakan - $penggajian->total_kasbon, + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Biaya makan berhasil diubah menjadi Rp ' . number_format($newMakan, 0, ',', '.') . '.' + ]); + + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => 'Gagal mengubah biaya makan: ' . $e->getMessage()], 500); + } + } + + /** + * Process all unpaid payments + */ + public function prosesSemuaPembayaran() + { + try { + $unpaidCount = Penggajian::belumBayar()->count(); + + if ($unpaidCount == 0) { + return response()->json(['success' => false, 'message' => 'Tidak ada pembayaran yang perlu diproses.']); + } + + Penggajian::belumBayar()->update(['status_pembayaran' => Penggajian::STATUS_SUDAH_BAYAR]); + + return response()->json([ + 'success' => true, + 'message' => "Berhasil memproses pembayaran untuk {$unpaidCount} teknisi." + ]); + + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => 'Gagal memproses pembayaran.'], 500); + } + } + + /** + * Generate slip gaji + */ + public function slip(Penggajian $penggajian) + { + $penggajian->load(['teknisi', 'detailPenggajian.penugasan']); + return view('admin.Gaji.slip_penggajian', compact('penggajian')); + } + + /** + * Export to Excel + */ + public function export(Request $request) + { + $query = Penggajian::with(['teknisi']); + + if ($request->filled('periode_bulan')) $query->where('periode_bulan', $request->periode_bulan); + if ($request->filled('periode_tahun')) $query->where('periode_tahun', $request->periode_tahun); + if ($request->filled('status_pembayaran')) $query->where('status_pembayaran', $request->status_pembayaran); + + $penggajians = $query->get(); + return Excel::download(new PenggajianExport($penggajians), 'penggajian_' . date('Y-m-d') . '.xlsx'); + } + + // ===================================================================== + // PRIVATE HELPERS + // ===================================================================== + + /** + * Calculate gaji untuk teknisi tertentu. + * + * Mencari semua penugasan di mana teknisi ini terlibat, baik sebagai: + * (a) Teknisi utama (kolom id_teknisi di tabel penugasans), ATAU + * (b) Anggota tim (tabel tim_teknisi_penugasans) dengan status_kehadiran = hadir + * + * Ongkos dibagi rata berdasarkan jumlah anggota tim yang hadir. + * Contoh: ongkos Rp 50.000, tim 2 orang → masing-masing Rp 25.000 + */ + private function calculateGajiTeknisi($idTeknisi, $bulan, $tahun, $tanggalLimit = null, $includeKasbon = true) + { + $jumlah_hari_kerja = Absensi::where('id_teknisi', $idTeknisi) + ->whereMonth('tanggal', $bulan) + ->whereYear('tanggal', $tahun) + ->where('status', 'hadir') + ->count(); + + // Cari semua penugasan yang melibatkan teknisi ini di bulan/tahun tsb + $penugasans = Penugasan::where(function ($q) use ($idTeknisi) { + // (a) Teknisi utama penugasan + $q->where('id_teknisi', $idTeknisi) + // (b) Anggota tim yang hadir + ->orWhereHas('timTeknisi', function ($sq) use ($idTeknisi) { + $sq->where('id_teknisi', $idTeknisi) + ->where('status_kehadiran', 'hadir'); + }); + }) + ->where('status_pekerjaan', 'selesai') + ->where(function ($q) use ($bulan, $tahun) { + // Filter berdasarkan bulan selesai + // Prioritas: tanggal_diselesaikan, fallback ke updated_at + $q->where(function ($q2) use ($bulan, $tahun) { + $q2->whereNotNull('tanggal_diselesaikan') + ->whereMonth('tanggal_diselesaikan', $bulan) + ->whereYear('tanggal_diselesaikan', $tahun); + })->orWhere(function ($q2) use ($bulan, $tahun) { + $q2->whereNull('tanggal_diselesaikan') + ->whereMonth('updated_at', $bulan) + ->whereYear('updated_at', $tahun); + }); + }) + ->with(['items.tarif', 'timTeknisi']) + ->get(); + + $gaji_kotor = 0; + $list_penugasan = []; + + foreach ($penugasans as $penugasan) { + // Hitung jumlah anggota tim yang hadir + // Jika tidak ada record tim → teknisi kerja sendiri → jumlah = 1 + $jumlahHadir = $penugasan->countTimHadir(); + if ($jumlahHadir === 0) { + $jumlahHadir = 1; + } + + // Hitung total ongkos dari penugasan_items + $totalOngkosTugas = 0; + if ($penugasan->items->count() > 0) { + foreach ($penugasan->items as $item) { + $itemTotal = (float) $item->total_nilai_pekerjaan; + if ($itemTotal <= 0) { + $itemTotal = $this->calculatePenugasanItemValue($item); + } + $totalOngkosTugas += $itemTotal; + } + } + + // Fallback: ambil dari total_nilai_pekerjaan penugasan induk atau tarif + if ($totalOngkosTugas <= 0) { + $totalOngkosTugas = $this->calculatePenugasanValue($penugasan); + } + + // Lewati jika ongkos masih 0 setelah semua fallback + if ($totalOngkosTugas <= 0) { + continue; + } + + // Bagian ongkos = total ongkos dibagi jumlah anggota tim yang hadir + $bagianOngkos = $totalOngkosTugas / $jumlahHadir; + $gaji_kotor += $bagianOngkos; + + $list_penugasan[] = [ + 'id_penugasan' => $penugasan->id_penugasan, + 'tanggal_selesai' => $penugasan->tanggal_diselesaikan ?? $penugasan->tanggal_diberikan, + 'lokasi' => $penugasan->alamat_lokasi + ?? $penugasan->lokasi_pekerjaan + ?? '-', + 'ongkos_penugasan' => $totalOngkosTugas, + 'jumlah_tim' => $jumlahHadir, + 'bagian_ongkos' => $bagianOngkos, + 'rincian_pekerjaan'=> $this->generateRincianLabel($penugasan), + ]; + } + + $biaya_makan = $jumlah_hari_kerja * 25000; + $total_kasbon = 0; + + if ($includeKasbon) { + $queryKasbon = Kasbon::where('id_teknisi', $idTeknisi) + ->where('status', 'belum_lunas'); + + if ($tanggalLimit) { + $queryKasbon->whereDate('tanggal_kasbon', '<=', $tanggalLimit); + } + + $total_kasbon = $queryKasbon->sum('jumlah_kasbon'); + } + + return [ + 'jumlah_hari_kerja' => $jumlah_hari_kerja, + 'gaji_kotor' => $gaji_kotor, + 'biaya_makan' => $biaya_makan, + 'total_kasbon' => $total_kasbon, + 'details' => $list_penugasan, + ]; + } + + /** + * Calculate hari kerja dari absensi + */ + private function calculateHariKerja($idTeknisi, $bulan, $tahun) + { + return Absensi::where('id_teknisi', $idTeknisi) + ->whereMonth('tanggal', $bulan) + ->whereYear('tanggal', $tahun) + ->where('status', 'hadir') + ->count(); + } + + /** + * Calculate nilai penugasan dengan fallback ke tarif + */ + private function calculatePenugasanValue($penugasan): float + { + if ($penugasan->total_nilai_pekerjaan > 0) { + return (float) $penugasan->total_nilai_pekerjaan; + } + + $tarif = $penugasan->tarif; + if (!$tarif) { + $query = TarifPekerjaan::where('jenis_pekerjaan', $penugasan->jenis_pekerjaan) + ->where('is_active', true); + if ($penugasan->dimensi_pipa) { + $query->where('dimensi_pipa', $penugasan->dimensi_pipa); + } + $tarif = $query->first(); + } + + if (!$tarif) return 0; + + if ($penugasan->jarak_meter > 0 && $tarif->tarif_per_meter) { + return (float) $tarif->tarif_per_meter * (float) $penugasan->jarak_meter; + } + if ($penugasan->jumlah_unit > 0 && $tarif->tarif_per_unit) { + return (float) $tarif->tarif_per_unit * (int) $penugasan->jumlah_unit; + } + if ($penugasan->jumlah_titik > 0 && $tarif->tarif_per_unit) { + return (float) $tarif->tarif_per_unit * (int) $penugasan->jumlah_titik; + } + + return (float) ($tarif->tarif_per_unit ?? $tarif->tarif_per_meter ?? 0); + } + + /** + * Generate label rincian pekerjaan untuk slip gaji + */ + private function generateRincianLabel($penugasan): string + { + $rincian = []; + + if ($penugasan->items && $penugasan->items->count() > 0) { + foreach ($penugasan->items as $item) { + $detail = $item->jenis_pekerjaan; + if ($item->jarak_meter > 0) { + $detail .= " ({$item->jarak_meter}m)"; + } elseif ($item->jumlah_unit > 0) { + $detail .= " ({$item->jumlah_unit} Unit)"; + } elseif ($item->jumlah_titik > 0) { + $detail .= " ({$item->jumlah_titik} Titik)"; + } + $rincian[] = $detail; + } + } else { + // Legacy: penugasan belum punya penugasan_items + if ($penugasan->jarak_meter > 0) { + $rincian[] = "{$penugasan->jarak_meter} Meter"; + } elseif ($penugasan->jumlah_unit > 0) { + $rincian[] = "{$penugasan->jumlah_unit} Unit"; + } else { + $rincian[] = "Borongan"; + } + } + + return implode(', ', $rincian); + } + + /** + * Calculate nilai untuk setiap PenugasanItem + */ + private function calculatePenugasanItemValue($item): float + { + if ($item->total_nilai_pekerjaan > 0) { + return (float) $item->total_nilai_pekerjaan; + } + + $tarif = $item->tarif; + if (!$tarif) { + $query = TarifPekerjaan::where('jenis_pekerjaan', $item->jenis_pekerjaan) + ->where('is_active', true); + if ($item->dimensi_pipa) { + $query->where('dimensi_pipa', $item->dimensi_pipa); + } + $tarif = $query->first(); + } + + if (!$tarif) return 0; + + if ($item->jarak_meter > 0 && $tarif->tarif_per_meter) { + return (float) $tarif->tarif_per_meter * (float) $item->jarak_meter; + } + if ($item->jumlah_unit > 0 && $tarif->tarif_per_unit) { + return (float) $tarif->tarif_per_unit * (int) $item->jumlah_unit; + } + if ($item->jumlah_titik > 0 && $tarif->tarif_per_unit) { + return (float) $tarif->tarif_per_unit * (int) $item->jumlah_titik; + } + + return (float) ($tarif->tarif_per_unit ?? $tarif->tarif_per_meter ?? 0); + } +} \ No newline at end of file diff --git a/samooapk/app/Http/Controllers/PenugasanController.php b/samooapk/app/Http/Controllers/PenugasanController.php new file mode 100644 index 0000000..7072eac --- /dev/null +++ b/samooapk/app/Http/Controllers/PenugasanController.php @@ -0,0 +1,406 @@ +filled('status')) { + if ($request->status === 'garansi_aktif') { + $query->garansiAktif(); + } else { + $query->where('status_pekerjaan', $request->status); + } + } + + if ($request->filled('jenis_pekerjaan')) { + $query->where('jenis_pekerjaan', $request->jenis_pekerjaan); + } + + if ($request->filled('search')) { + $search = $request->search; + $query->where(function($q) use ($search) { + $q->where('catatan_admin', 'LIKE', "%{$search}%") + ->orWhere('detail_pekerjaan', 'LIKE', "%{$search}%") + ->orWhereHas('teknisi', function($tq) use ($search) { + $tq->where('nama', 'LIKE', "%{$search}%"); + }); + }); + } + + if ($request->filled('id_teknisi')) { + $teknisiId = $request->id_teknisi; + $query->where(function($q) use ($teknisiId) { + $q->where('id_teknisi', $teknisiId) + ->orWhereHas('timTeknisi', function($tq) use ($teknisiId) { + $tq->where('id_teknisi', $teknisiId); + }); + }); + } + + if ($request->filled('start_date') && $request->filled('end_date')) { + $query->whereBetween('tanggal_diberikan', [$request->start_date, $request->end_date]); + } elseif ($request->filled('start_date')) { + $query->where('tanggal_diberikan', '>=', $request->start_date); + } elseif ($request->filled('end_date')) { + $query->where('tanggal_diberikan', '<=', $request->end_date); + } + + $penugasan = $query->orderBy('created_at', 'desc')->paginate(10); + + $totalPenugasan = Penugasan::count(); + $belumMulai = Penugasan::where('status_pekerjaan', 'belum_mulai')->count(); + $dalamProses = Penugasan::where('status_pekerjaan', 'dalam_proses')->count(); + $selesai = Penugasan::where('status_pekerjaan', 'selesai')->count(); + $dibatalkan = Penugasan::where('status_pekerjaan', 'dibatalkan')->count(); + $garansiAktif = Penugasan::garansiAktif()->count(); + + $today = Carbon::today()->toDateString(); + $absentTechIds = \App\Models\Absensi::where('tanggal', $today) + ->whereIn('status', ['izin', 'sakit']) + ->pluck('id_teknisi') + ->toArray(); + + $abandonedTasks = collect(); + if (!empty($absentTechIds)) { + $abandonedTasks = Penugasan::whereIn('status_pekerjaan', ['belum_mulai', 'dalam_proses']) + ->where(function($q) use ($absentTechIds) { + $q->whereIn('id_teknisi', $absentTechIds) + ->orWhereHas('timTeknisi', function($tq) use ($absentTechIds) { + $tq->whereIn('id_teknisi', $absentTechIds); + }); + }) + ->with('teknisi') + ->get(); + } + + $teknisis = Teknisi::where('status', 'aktif')->orderBy('nama')->get(); + $teknisiList = $teknisis; // Use same data for the new dropdown + + return view('Admin.KelolaPekerjaan.Penugasan', compact( + 'penugasan', 'totalPenugasan', 'belumMulai', 'dalamProses', + 'selesai', 'dibatalkan', 'garansiAktif', 'teknisis', 'teknisiList', 'abandonedTasks' + )); + } + + public function store(Request $request) + { + try { + $validated = $request->validate([ + 'id_teknisi' => 'required|array', + 'id_teknisi.*' => 'exists:teknisis,id_teknisi', + 'tanggal_diberikan' => 'required|date', + 'foto_surat' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:5120', + 'catatan_admin' => 'nullable|string', + 'jenis_pekerjaan' => 'required|string', + 'alamat_lokasi' => 'required|string', + 'nama_pelanggan' => 'nullable|string', + 'no_sambungan' => 'nullable|string', + ]); + + DB::beginTransaction(); + + $path = null; + if ($request->hasFile('foto_surat')) { + $path = $request->file('foto_surat')->store('penugasan/surat', 'public'); + } + + $penugasan = Penugasan::create([ + 'id_teknisi' => $validated['id_teknisi'][0], + 'foto_surat' => $path, + 'tanggal_diberikan' => $validated['tanggal_diberikan'], + 'catatan_admin' => $validated['catatan_admin'] ?? null, + 'jenis_pekerjaan' => $validated['jenis_pekerjaan'], + 'alamat_lokasi' => $validated['alamat_lokasi'], + 'nama_pelanggan' => $validated['nama_pelanggan'] ?? null, + 'no_sambungan' => $validated['no_sambungan'] ?? null, + 'status_pekerjaan' => 'belum_mulai', + ]); + + $techIds = array_unique($validated['id_teknisi']); + foreach ($techIds as $techId) { + TimTeknisiPenugasan::create([ + 'id_penugasan' => $penugasan->id_penugasan, + 'id_teknisi' => $techId, + 'status_kehadiran' => 'hadir', + ]); + } + + DB::commit(); + + return redirect()->route('pekerjaan.penugasan.index') + ->with('success', 'Penugasan berhasil dibuat! Teknisi akan melengkapi detail via mobile.'); + + } catch (\Exception $e) { + DB::rollBack(); + return redirect()->back() + ->with('error', 'Gagal menambahkan data: ' . $e->getMessage()) + ->withInput(); + } + } + + /** + * ✅ FIX: Tambah field garansi lengkap di response show() + * - is_garansi_aktif → true/false apakah garansi masih aktif + * - sisa_hari_garansi → berapa hari tersisa + * - tanggal_garansi_mulai → sudah ikut dari toArray() + * - tanggal_garansi_selesai → sudah ikut dari toArray() + * + * Sebelumnya field-field ini tidak di-append sehingga + * badge garansi di modal detail tidak pernah muncul + * meski datanya sudah ada di database + */ + public function show($id) + { + try { + $penugasan = Penugasan::with(['teknisi', 'tarif', 'timTeknisi.teknisi', 'items.tarif']) + ->findOrFail($id); + + $data = $penugasan->toArray(); + + $data['foto_surat_url'] = $penugasan->foto_surat_url; + $data['foto_sebelum_url'] = $penugasan->foto_sebelum_url; + $data['foto_sesudah_url'] = $penugasan->foto_sesudah_url; + $data['label_jenis_pekerjaan'] = $penugasan->label_jenis_pekerjaan; + + // ✅ FIX: Info garansi — dibutuhkan oleh blade untuk tampilkan badge + $data['is_garansi_aktif'] = $penugasan->isGaransiAktif(); + $data['sisa_hari_garansi'] = $penugasan->getSisaHariGaransi(); + + return response()->json(['success' => true, 'data' => $data]); + + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404); + } + } + + public function edit($id) + { + try { + $penugasan = Penugasan::with(['teknisi', 'timTeknisi.teknisi'])->findOrFail($id); + + $data = $penugasan->toArray(); + $data['foto_surat_url'] = $penugasan->foto_surat_url; + + return response()->json(['success' => true, 'data' => $data]); + + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404); + } + } + + public function update(Request $request, $id) + { + try { + $penugasan = Penugasan::findOrFail($id); + + $validated = $request->validate([ + 'id_teknisi' => 'required|array', + 'id_teknisi.*' => 'exists:teknisis,id_teknisi', + 'tanggal_diberikan' => 'required|date', + 'foto_surat' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:5120', + 'catatan_admin' => 'nullable|string', + 'jenis_pekerjaan' => 'required|string', + 'alamat_lokasi' => 'required|string', + 'nama_pelanggan' => 'nullable|string', + 'no_sambungan' => 'nullable|string', + ]); + + DB::beginTransaction(); + + $updateData = [ + 'id_teknisi' => $validated['id_teknisi'][0], + 'tanggal_diberikan' => $validated['tanggal_diberikan'], + 'catatan_admin' => $validated['catatan_admin'] ?? null, + 'jenis_pekerjaan' => $validated['jenis_pekerjaan'], + 'alamat_lokasi' => $validated['alamat_lokasi'], + 'nama_pelanggan' => $validated['nama_pelanggan'] ?? null, + 'no_sambungan' => $validated['no_sambungan'] ?? null, + ]; + + if ($request->hasFile('foto_surat')) { + if ($penugasan->foto_surat) { + Storage::disk('public')->delete($penugasan->foto_surat); + } + $updateData['foto_surat'] = $request->file('foto_surat')->store('penugasan/surat', 'public'); + } + + $penugasan->update($updateData); + + TimTeknisiPenugasan::where('id_penugasan', $penugasan->id_penugasan)->delete(); + + $techIds = array_unique($validated['id_teknisi']); + foreach ($techIds as $techId) { + TimTeknisiPenugasan::create([ + 'id_penugasan' => $penugasan->id_penugasan, + 'id_teknisi' => $techId, + 'status_kehadiran' => 'hadir', + ]); + } + + DB::commit(); + + return response()->json(['success' => true, 'message' => 'Data berhasil diupdate!']); + + } catch (\Exception $e) { + DB::rollBack(); + return response()->json(['success' => false, 'message' => 'Gagal update: ' . $e->getMessage()], 500); + } + } + + public function destroy($id) + { + try { + DB::beginTransaction(); + + $penugasan = Penugasan::findOrFail($id); + + if ($penugasan->foto_surat) { + Storage::disk('public')->delete($penugasan->foto_surat); + } + + $penugasan->delete(); + + DB::commit(); + + return response()->json(['success' => true, 'message' => 'Data berhasil dihapus!']); + + } catch (\Exception $e) { + DB::rollBack(); + return response()->json(['success' => false, 'message' => 'Gagal menghapus: ' . $e->getMessage()], 500); + } + } + + public function getTarifByJenis(Request $request) + { + try { + $tarifs = TarifPekerjaan::where('jenis_pekerjaan', $request->jenis_pekerjaan) + ->where('is_active', true) + ->get(); + + return response()->json(['success' => true, 'data' => $tarifs]); + + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => 'Gagal mengambil tarif'], 500); + } + } + + public function getTeknisiByDate(Request $request) + { + try { + // Selalu tampilkan seluruh teknisi aktif agar fleksibel secara operasional + $teknisis = Teknisi::where('status', 'aktif') + ->orderBy('nama') + ->get(); + + return response()->json(['success' => true, 'data' => $teknisis]); + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => 'Gagal mengambil data teknisi: ' . $e->getMessage()], 500); + } + } + + + /** + * ✅ FIX: Filter tanggal monitoring pakai tanggal_diselesaikan + * bukan updated_at agar hasil filter akurat + */ + public function monitoring(Request $request) + { + $query = Penugasan::with(['teknisi', 'timTeknisi.teknisi']); + + if ($request->filled('status')) { + $query->where('status_pekerjaan', $request->status); + } + + if ($request->filled('teknisi_id')) { + $teknisiId = $request->teknisi_id; + $query->where(function($q) use ($teknisiId) { + $q->where('id_teknisi', $teknisiId) + ->orWhereHas('timTeknisi', function($tq) use ($teknisiId) { + $tq->where('id_teknisi', $teknisiId); + }); + }); + } + + if ($request->filled('start_date') && $request->filled('end_date')) { + $start = $request->start_date . ' 00:00:00'; + $end = $request->end_date . ' 23:59:59'; + $query->where(function($q) use ($start, $end) { + $q->where(function($q2) use ($start, $end) { + $q2->whereNotNull('tanggal_diselesaikan') + ->whereBetween('tanggal_diselesaikan', [$start, $end]); + })->orWhere(function($q2) use ($start, $end) { + $q2->whereNull('tanggal_diselesaikan') + ->whereBetween('updated_at', [$start, $end]); + }); + }); + } elseif ($request->filled('start_date')) { + $start = $request->start_date . ' 00:00:00'; + $query->where(function($q) use ($start) { + $q->where(function($q2) use ($start) { + $q2->whereNotNull('tanggal_diselesaikan') + ->where('tanggal_diselesaikan', '>=', $start); + })->orWhere(function($q2) use ($start) { + $q2->whereNull('tanggal_diselesaikan') + ->where('updated_at', '>=', $start); + }); + }); + } elseif ($request->filled('end_date')) { + $end = $request->end_date . ' 23:59:59'; + $query->where(function($q) use ($end) { + $q->where(function($q2) use ($end) { + $q2->whereNotNull('tanggal_diselesaikan') + ->where('tanggal_diselesaikan', '<=', $end); + })->orWhere(function($q2) use ($end) { + $q2->whereNull('tanggal_diselesaikan') + ->where('updated_at', '<=', $end); + }); + }); + } + + $progresKerja = $query->orderBy('updated_at', 'desc')->paginate(12); + + $progresKerja->getCollection()->transform(function ($item) { + $item->persentase_pekerjaan = match($item->status_pekerjaan) { + 'selesai' => 100, + 'dalam_proses' => 50, + default => 0, + }; + $item->is_garansi_aktif = $item->isGaransiAktif(); + $item->sisa_hari_garansi = $item->getSisaHariGaransi(); + return $item; + }); + + $statistics = [ + 'total' => Penugasan::count(), + 'dalam_progres' => Penugasan::where('status_pekerjaan', 'dalam_proses')->count(), + 'selesai' => Penugasan::where('status_pekerjaan', 'selesai')->count(), + 'belum_mulai' => Penugasan::where('status_pekerjaan', 'belum_mulai')->count(), + 'garansi_aktif' => Penugasan::garansiAktif()->count(), + ]; + + $statusList = [ + 'belum_mulai' => 'Belum Mulai', + 'dalam_proses' => 'Dalam Proses', + 'selesai' => 'Selesai', + ]; + + $teknisiList = Teknisi::orderBy('nama')->get(); + + return view('Admin.KelolaPekerjaan.ProgresKerja', compact( + 'progresKerja', 'statistics', 'statusList', 'teknisiList' + )); + } +} \ No newline at end of file diff --git a/samooapk/app/Http/Controllers/ProfileController.php b/samooapk/app/Http/Controllers/ProfileController.php new file mode 100644 index 0000000..a48eb8d --- /dev/null +++ b/samooapk/app/Http/Controllers/ProfileController.php @@ -0,0 +1,60 @@ + $request->user(), + ]); + } + + /** + * Update the user's profile information. + */ + public function update(ProfileUpdateRequest $request): RedirectResponse + { + $request->user()->fill($request->validated()); + + if ($request->user()->isDirty('email')) { + $request->user()->email_verified_at = null; + } + + $request->user()->save(); + + return Redirect::route('profile.edit')->with('status', 'profile-updated'); + } + + /** + * Delete the user's account. + */ + public function destroy(Request $request): RedirectResponse + { + $request->validateWithBag('userDeletion', [ + 'password' => ['required', 'current_password'], + ]); + + $user = $request->user(); + + Auth::logout(); + + $user->delete(); + + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return Redirect::to('/'); + } +} diff --git a/samooapk/app/Http/Controllers/ProgresKerjaController.php b/samooapk/app/Http/Controllers/ProgresKerjaController.php new file mode 100644 index 0000000..dc136e0 --- /dev/null +++ b/samooapk/app/Http/Controllers/ProgresKerjaController.php @@ -0,0 +1,464 @@ +filled('status')) { + $query->byStatus($request->status); + } + + // Filter by teknisi + if ($request->filled('teknisi_id')) { + $query->byTeknisi($request->teknisi_id); + } + + // Filter by date range + if ($request->filled('start_date') || $request->filled('end_date')) { + $query->byDateRange($request->start_date, $request->end_date); + } + + // Search functionality + if ($request->filled('search')) { + $search = $request->search; + $query->where(function($q) use ($search) { + $q->where('tugas', 'like', "%{$search}%") + ->orWhere('deskripsi_progres', 'like', "%{$search}%") + ->orWhereHas('teknisi', function($teknisiQuery) use ($search) { + $teknisiQuery->where('nama', 'like', "%{$search}%"); + }); + }); + } + + $progresKerja = $query->latest()->paginate(10); + + // Get filter options + $teknisiList = Teknisi::orderBy('nama')->get(); + $statusList = ProgresKerja::$statusProgres; + + // Get statistics + $statistics = ProgresKerja::getStatistics(); + + return view('Admin.KelolaPekerjaan.ProgresKerja', compact( + 'progresKerja', + 'teknisiList', + 'statusList', + 'statistics' + )); + } + + /** + * Show the form for creating a new progress kerja. + * + * @return \Illuminate\Http\Response + */ + public function create() + { + $penugasanList = Penugasan::with('pelanggan')->orderBy('created_at', 'desc')->get(); + $teknisiList = Teknisi::orderBy('nama')->get(); + $statusList = ProgresKerja::$statusProgres; + + return view('Admin.KelolaPekerjaan.ProgresKerjaForm', compact( + 'penugasanList', + 'teknisiList', + 'statusList' + )); + } + + /** + * Store a newly created progress kerja in storage. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + */ + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'id_penugasan' => 'required|exists:penugasan,id_penugasan', + 'id_teknisi' => 'required|exists:teknisi,id_teknisi', + 'tugas' => 'required|string|max:255', + 'deskripsi_progres' => 'nullable|string', + 'status_progres' => 'required|in:' . implode(',', array_keys(ProgresKerja::$statusProgres)), + 'persentase_pekerjaan' => 'required|integer|min:0|max:100', + 'foto_progress' => 'nullable|image|mimes:jpeg,png,jpg|max:2048', + ]); + + if ($validator->fails()) { + return redirect()->back() + ->withErrors($validator) + ->withInput(); + } + + $data = $request->only([ + 'id_penugasan', + 'id_teknisi', + 'tugas', + 'deskripsi_progres', + 'status_progres', + 'persentase_pekerjaan' + ]); + + // Handle foto progress upload + if ($request->hasFile('foto_progress')) { + $file = $request->file('foto_progress'); + $fileName = time() . '_' . Str::random(10) . '.' . $file->getClientOriginalExtension(); + + // Store in public/storage/progress_photos + $file->storeAs('progress_photos', $fileName, 'public'); + $data['foto_progress'] = $fileName; + } + + $progresKerja = ProgresKerja::create($data); + + // Auto update status based on percentage + $progresKerja->autoUpdateStatus(); + + return redirect()->route('progres-kerja.index') + ->with('success', 'Progress kerja berhasil ditambahkan.'); + } + + /** + * Display the specified progress kerja. + * + * @param \App\Models\ProgresKerja $progresKerja + * @return \Illuminate\Http\Response + */ + public function show(ProgresKerja $progresKerja) + { + $progresKerja->load(['teknisi', 'penugasan.pelanggan']); + + return view('Admin.KelolaPekerjaan.ProgresKerjaDetail', compact('progresKerja')); + } + + /** + * Show the form for editing the specified progress kerja. + * + * @param \App\Models\ProgresKerja $progresKerja + * @return \Illuminate\Http\Response + */ + public function edit(ProgresKerja $progresKerja) + { + $penugasanList = Penugasan::with('pelanggan')->orderBy('created_at', 'desc')->get(); + $teknisiList = Teknisi::orderBy('nama')->get(); + $statusList = ProgresKerja::$statusProgres; + + return view('Admin.KelolaPekerjaan.ProgresKerjaForm', compact( + 'progresKerja', + 'penugasanList', + 'teknisiList', + 'statusList' + )); + } + + /** + * Update the specified progress kerja in storage. + * + * @param \Illuminate\Http\Request $request + * @param \App\Models\ProgresKerja $progresKerja + * @return \Illuminate\Http\Response + */ + public function update(Request $request, ProgresKerja $progresKerja) + { + $validator = Validator::make($request->all(), [ + 'id_penugasan' => 'required|exists:penugasan,id_penugasan', + 'id_teknisi' => 'required|exists:teknisi,id_teknisi', + 'tugas' => 'required|string|max:255', + 'deskripsi_progres' => 'nullable|string', + 'status_progres' => 'required|in:' . implode(',', array_keys(ProgresKerja::$statusProgres)), + 'persentase_pekerjaan' => 'required|integer|min:0|max:100', + 'foto_progress' => 'nullable|image|mimes:jpeg,png,jpg|max:2048', + ]); + + if ($validator->fails()) { + return redirect()->back() + ->withErrors($validator) + ->withInput(); + } + + $data = $request->only([ + 'id_penugasan', + 'id_teknisi', + 'tugas', + 'deskripsi_progres', + 'status_progres', + 'persentase_pekerjaan' + ]); + + // Handle foto progress upload + if ($request->hasFile('foto_progress')) { + // Delete old photo if exists + if ($progresKerja->foto_progress) { + Storage::disk('public')->delete('progress_photos/' . $progresKerja->foto_progress); + } + + $file = $request->file('foto_progress'); + $fileName = time() . '_' . Str::random(10) . '.' . $file->getClientOriginalExtension(); + + $file->storeAs('progress_photos', $fileName, 'public'); + $data['foto_progress'] = $fileName; + } + + $progresKerja->update($data); + + // Auto update status based on percentage + $progresKerja->autoUpdateStatus(); + + return redirect()->route('progres-kerja.index') + ->with('success', 'Progress kerja berhasil diperbarui.'); + } + + /** + * Remove the specified progress kerja from storage. + * + * @param \App\Models\ProgresKerja $progresKerja + * @return \Illuminate\Http\Response + */ + public function destroy(ProgresKerja $progresKerja) + { + // Delete photo if exists + if ($progresKerja->foto_progress) { + Storage::disk('public')->delete('progress_photos/' . $progresKerja->foto_progress); + } + + $progresKerja->delete(); + + return redirect()->route('progres-kerja.index') + ->with('success', 'Progress kerja berhasil dihapus.'); + } + + /** + * Quick update progress status + * + * @param \Illuminate\Http\Request $request + * @param \App\Models\ProgresKerja $progresKerja + * @return \Illuminate\Http\JsonResponse + */ + public function updateStatus(Request $request, ProgresKerja $progresKerja) + { + $validator = Validator::make($request->all(), [ + 'status' => 'required|in:' . implode(',', array_keys(ProgresKerja::$statusProgres)), + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Status tidak valid.' + ], 400); + } + + // Check if transition is valid + if (!$progresKerja->canTransitionTo($request->status)) { + return response()->json([ + 'success' => false, + 'message' => 'Transisi status tidak valid.' + ], 400); + } + + $progresKerja->update(['status_progres' => $request->status]); + + return response()->json([ + 'success' => true, + 'message' => 'Status berhasil diperbarui.', + 'new_status' => $progresKerja->status_formatted, + 'badge_class' => $progresKerja->status_badge_class + ]); + } + + /** + * Update progress percentage + * + * @param \Illuminate\Http\Request $request + * @param \App\Models\ProgresKerja $progresKerja + * @return \Illuminate\Http\JsonResponse + */ + public function updatePercentage(Request $request, ProgresKerja $progresKerja) + { + $validator = Validator::make($request->all(), [ + 'percentage' => 'required|integer|min:0|max:100', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Persentase tidak valid.' + ], 400); + } + + $progresKerja->update(['persentase_pekerjaan' => $request->percentage]); + $progresKerja->autoUpdateStatus(); + + return response()->json([ + 'success' => true, + 'message' => 'Persentase berhasil diperbarui.', + 'new_percentage' => $progresKerja->persentase_pekerjaan, + 'new_status' => $progresKerja->status_formatted, + 'badge_class' => $progresKerja->status_badge_class + ]); + } + + /** + * Get progress by teknisi (AJAX) + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function getByTeknisi(Request $request) + { + $teknisiId = $request->get('teknisi_id'); + + if (!$teknisiId) { + return response()->json([ + 'success' => false, + 'message' => 'Teknisi ID diperlukan.' + ], 400); + } + + $progresKerja = ProgresKerja::getByTeknisi($teknisiId); + + return response()->json([ + 'success' => true, + 'data' => $progresKerja->map(function($item) { + return [ + 'id' => $item->id_progres, + 'tugas' => $item->tugas, + 'status' => $item->status_formatted, + 'persentase' => $item->persentase_pekerjaan, + 'tanggal_update' => $item->tanggal_update_formatted, + 'penugasan' => $item->penugasan->nama_pekerjaan ?? 'N/A' + ]; + }) + ]); + } + + /** + * Export progress data + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + */ + public function export(Request $request) + { + $query = ProgresKerja::with(['teknisi', 'penugasan']); + + // Apply same filters as index + if ($request->filled('status')) { + $query->byStatus($request->status); + } + + if ($request->filled('teknisi_id')) { + $query->byTeknisi($request->teknisi_id); + } + + if ($request->filled('start_date') || $request->filled('end_date')) { + $query->byDateRange($request->start_date, $request->end_date); + } + + $progresKerja = $query->latest()->get(); + + $filename = 'progress_kerja_' . date('Y-m-d') . '.csv'; + + $headers = [ + 'Content-Type' => 'text/csv', + 'Content-Disposition' => 'attachment; filename="' . $filename . '"', + ]; + + $callback = function() use ($progresKerja) { + $file = fopen('php://output', 'w'); + + // Add BOM for UTF-8 + fwrite($file, "\xEF\xBB\xBF"); + + // Header row + fputcsv($file, [ + 'ID', + 'Penugasan', + 'Teknisi', + 'Tugas', + 'Deskripsi Progress', + 'Status', + 'Persentase (%)', + 'Tanggal Update', + 'Foto Progress' + ]); + + // Data rows + foreach ($progresKerja as $item) { + fputcsv($file, [ + $item->id_progres, + $item->penugasan->nama_pekerjaan ?? 'N/A', + $item->teknisi->nama ?? 'N/A', + $item->tugas, + $item->deskripsi_progres, + $item->status_formatted, + $item->persentase_pekerjaan, + $item->tanggal_update_formatted, + $item->has_foto ? 'Ya' : 'Tidak' + ]); + } + + fclose($file); + }; + + return response()->stream($callback, 200, $headers); + } + + /** + * Get recent updates for dashboard + * + * @return \Illuminate\Http\JsonResponse + */ + public function getRecentUpdates() + { + $recentUpdates = ProgresKerja::getRecentUpdates(5); + + return response()->json([ + 'success' => true, + 'data' => $recentUpdates->map(function($item) { + return [ + 'id' => $item->id_progres, + 'tugas' => $item->tugas, + 'teknisi' => $item->teknisi->nama ?? 'N/A', + 'status' => $item->status_formatted, + 'persentase' => $item->persentase_pekerjaan, + 'tanggal_update' => $item->tanggal_update_formatted, + 'days_since_update' => $item->days_since_update + ]; + }) + ]); + } + + /** + * Get statistics for dashboard + * + * @return \Illuminate\Http\JsonResponse + */ + public function getStatistics() + { + $statistics = ProgresKerja::getStatistics(); + + return response()->json([ + 'success' => true, + 'data' => $statistics + ]); + } +} \ No newline at end of file diff --git a/samooapk/app/Http/Controllers/TeknisiController.php b/samooapk/app/Http/Controllers/TeknisiController.php new file mode 100644 index 0000000..42b610f --- /dev/null +++ b/samooapk/app/Http/Controllers/TeknisiController.php @@ -0,0 +1,346 @@ +has('q') && $request->q != '') { + $searchTerm = $request->q; + $query->where(function($q) use ($searchTerm) { + $q->where('nama', 'LIKE', "%{$searchTerm}%") + ->orWhere('email', 'LIKE', "%{$searchTerm}%") + ->orWhere('no_telephone', 'LIKE', "%{$searchTerm}%"); + }); + } + + $teknisis = $query->latest()->get(); + + // Jika request dari AJAX, return JSON + if ($request->ajax() || $request->wantsJson()) { + return response()->json([ + 'success' => true, + 'data' => $teknisis + ]); + } + + // Jika request biasa, return view + return view('Admin.KelolaTeknisi.Teknisi', compact('teknisis')); + } + + /** + * Show the form for creating a new resource. + */ + public function create() + { + return view('teknisi.create'); + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'nama' => 'required|string|max:100', + 'tanggal_lahir' => 'required|date', + 'alamat' => 'required|string', + 'email' => 'nullable|email|max:100|unique:teknisis,email', + 'no_telephone' => 'required|string|max:15', + 'tanggal_masuk' => 'required|date', + 'status' => 'required|in:aktif,tidak_aktif', + ], [ + 'nama.required' => 'Nama wajib diisi', + 'nama.max' => 'Nama maksimal 100 karakter', + 'tanggal_lahir.required' => 'Tanggal lahir wajib diisi', + 'tanggal_lahir.date' => 'Format tanggal lahir tidak valid', + 'alamat.required' => 'Alamat wajib diisi', + 'email.email' => 'Format email tidak valid', + 'email.unique' => 'Email sudah terdaftar', + 'no_telephone.required' => 'Nomor telephone wajib diisi', + 'no_telephone.max' => 'Nomor telephone maksimal 15 karakter', + 'tanggal_masuk.required' => 'Tanggal masuk wajib diisi', + 'tanggal_masuk.date' => 'Format tanggal masuk tidak valid', + 'status.required' => 'Status wajib dipilih', + 'status.in' => 'Status harus aktif atau tidak_aktif', + ]); + + if ($validator->fails()) { + // Jika AJAX request + if (request()->ajax() || request()->wantsJson()) { + return response()->json([ + 'success' => false, + 'errors' => $validator->errors() + ], 422); + } + + return redirect()->back() + ->withErrors($validator) + ->withInput(); + } + + try { + $teknisi = Teknisi::create($request->all()); + + // Jika AJAX request + if (request()->ajax() || request()->wantsJson()) { + return response()->json([ + 'success' => true, + 'message' => 'Data teknisi berhasil ditambahkan', + 'data' => $teknisi + ], 201); + } + + return redirect()->route('teknisi.index') + ->with('success', 'Data teknisi berhasil ditambahkan'); + } catch (\Exception $e) { + Log::error('Error creating teknisi: ' . $e->getMessage()); + + // Jika AJAX request + if (request()->ajax() || request()->wantsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal menambahkan data teknisi: ' . $e->getMessage() + ], 500); + } + + return redirect()->back() + ->with('error', 'Gagal menambahkan data teknisi: ' . $e->getMessage()) + ->withInput(); + } + } + + /** + * Display the specified resource. + */ + public function show(string $id) + { + try { + $teknisi = Teknisi::where('id_teknisi', $id)->firstOrFail(); + + // Jika AJAX request, return JSON + if (request()->ajax() || request()->wantsJson()) { + return response()->json([ + 'success' => true, + 'data' => $teknisi + ]); + } + + return view('teknisi.show', compact('teknisi')); + } catch (\Exception $e) { + if (request()->ajax() || request()->wantsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Data teknisi tidak ditemukan' + ], 404); + } + + return redirect()->route('teknisi.index') + ->with('error', 'Data teknisi tidak ditemukan'); + } + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(string $id) + { + try { + $teknisi = Teknisi::where('id_teknisi', $id)->firstOrFail(); + return view('teknisi.edit', compact('teknisi')); + } catch (\Exception $e) { + return redirect()->route('teknisi.index') + ->with('error', 'Data teknisi tidak ditemukan'); + } + } + + /** + * Update the specified resource in storage. + */ + + public function update(Request $request, string $id) +{ + try { + $teknisi = Teknisi::where('id_teknisi', $id)->firstOrFail(); + + // Log untuk debugging + Log::info('Update Teknisi Request', [ + 'id' => $id, + 'request_data' => $request->all() + ]); + + // Validasi - TANPA tanggal_lahir dan tanggal_masuk karena tidak boleh diubah + $validator = Validator::make($request->all(), [ + 'nama' => 'required|string|max:100', + 'alamat' => 'required|string', + 'email' => 'nullable|email|max:100|unique:teknisis,email,' . $id . ',id_teknisi', + 'no_telephone' => 'required|string|max:15', + 'status' => 'required|in:aktif,tidak_aktif', + 'tanggal_masuk' => 'required|date', + ], [ + 'nama.required' => 'Nama wajib diisi', + 'nama.max' => 'Nama maksimal 100 karakter', + 'alamat.required' => 'Alamat wajib diisi', + 'email.email' => 'Format email tidak valid', + 'email.unique' => 'Email sudah terdaftar', + 'no_telephone.required' => 'Nomor telephone wajib diisi', + 'no_telephone.max' => 'Nomor telephone maksimal 15 karakter', + 'status.required' => 'Status wajib dipilih', + 'status.in' => 'Status harus aktif atau tidak_aktif', + 'tanggal_masuk.required' => 'Tanggal masuk wajib diisi', + 'tanggal_masuk.date' => 'Format tanggal masuk tidak valid', + ]); + + // Allow deactivation even if unpaid kasbon; a warning will be shown in the UI. + // The check is now performed in the view for visual indication. + + + if ($validator->fails()) { + Log::warning('Validation Failed', [ + 'errors' => $validator->errors()->toArray() + ]); + + // Jika AJAX request + if (request()->ajax() || request()->wantsJson()) { + return response()->json([ + 'success' => false, + 'errors' => $validator->errors() + ], 422); + } + + return redirect()->back() + ->withErrors($validator) + ->withInput(); + } + + // Update data + $teknisi->update([ + 'nama' => $request->nama, + 'alamat' => $request->alamat, + 'email' => $request->email, + 'no_telephone' => $request->no_telephone, + 'status' => $request->status, + 'tanggal_masuk' => $request->tanggal_masuk, + ]); + + Log::info('Teknisi Updated Successfully', [ + 'id' => $id, + 'updated_data' => $teknisi->fresh()->toArray() + ]); + + // Jika AJAX request + if (request()->ajax() || request()->wantsJson()) { + return response()->json([ + 'success' => true, + 'message' => 'Data teknisi berhasil diperbarui', + 'data' => $teknisi + ]); + } + + return redirect()->route('teknisi.index') + ->with('success', 'Data teknisi berhasil diperbarui'); + + } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { + Log::error('Teknisi Not Found', ['id' => $id]); + + // Jika AJAX request + if (request()->ajax() || request()->wantsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Data teknisi tidak ditemukan' + ], 404); + } + + return redirect()->route('teknisi.index') + ->with('error', 'Data teknisi tidak ditemukan'); + + } catch (\Exception $e) { + Log::error('Update Teknisi Failed', [ + 'id' => $id, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + // Jika AJAX request + if (request()->ajax() || request()->wantsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal memperbarui data teknisi: ' . $e->getMessage() + ], 500); + } + + return redirect()->back() + ->with('error', 'Gagal memperbarui data teknisi: ' . $e->getMessage()) + ->withInput(); + } +} + /** + * Search teknisi + */ + public function search(Request $request) + { + $query = $request->get('q'); + + $teknisis = Teknisi::where('nama', 'LIKE', "%{$query}%") + ->orWhere('email', 'LIKE', "%{$query}%") + ->orWhere('no_telephone', 'LIKE', "%{$query}%") + ->latest() + ->get(); + + // Jika AJAX request + if (request()->ajax() || request()->wantsJson()) { + return response()->json([ + 'success' => true, + 'data' => $teknisis + ]); + } + + return view('Admin.KelolaTeknisi.Teknisi', compact('teknisis')); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(string $id) + { + try { + $teknisi = Teknisi::where('id_teknisi', $id)->firstOrFail(); + + // Cek tunggakan kasbon + $unpaidKasbon = \App\Models\Kasbon::where('id_teknisi', $id) + ->where('status', 'belum_lunas') + ->sum('jumlah_kasbon'); + + if ($unpaidKasbon > 0) { + return response()->json([ + 'success' => false, + 'message' => 'Teknisi tidak dapat dihapus karena masih memiliki tunggakan kasbon sebesar Rp ' . number_format($unpaidKasbon, 0, ',', '.') . '.' + ], 422); + } + + $teknisi->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Data teknisi berhasil dihapus' + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal menghapus data teknisi: ' . $e->getMessage() + ], 500); + } + } +} \ No newline at end of file diff --git a/samooapk/app/Http/Kernel.php b/samooapk/app/Http/Kernel.php new file mode 100644 index 0000000..494c050 --- /dev/null +++ b/samooapk/app/Http/Kernel.php @@ -0,0 +1,68 @@ + + */ + protected $middleware = [ + // \App\Http\Middleware\TrustHosts::class, + \App\Http\Middleware\TrustProxies::class, + \Illuminate\Http\Middleware\HandleCors::class, + \App\Http\Middleware\PreventRequestsDuringMaintenance::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 array> + */ + protected $middlewareGroups = [ + 'web' => [ + \App\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\VerifyCsrfToken::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ], + + 'api' => [ + // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, + \Illuminate\Routing\Middleware\ThrottleRequests::class.':api', + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ], + ]; + + /** + * The application's middleware aliases. + * + * Aliases may be used instead of class names to conveniently assign middleware to routes and groups. + * + * @var array + */ + protected $middlewareAliases = [ + 'auth' => \App\Http\Middleware\Authenticate::class, + 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, + 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, + 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, + 'can' => \Illuminate\Auth\Middleware\Authorize::class, + 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, + 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, + 'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class, + 'signed' => \App\Http\Middleware\ValidateSignature::class, + 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, + 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + ]; +} diff --git a/samooapk/app/Http/Middleware/Authenticate.php b/samooapk/app/Http/Middleware/Authenticate.php new file mode 100644 index 0000000..d4ef644 --- /dev/null +++ b/samooapk/app/Http/Middleware/Authenticate.php @@ -0,0 +1,17 @@ +expectsJson() ? null : route('login'); + } +} diff --git a/samooapk/app/Http/Middleware/EncryptCookies.php b/samooapk/app/Http/Middleware/EncryptCookies.php new file mode 100644 index 0000000..867695b --- /dev/null +++ b/samooapk/app/Http/Middleware/EncryptCookies.php @@ -0,0 +1,17 @@ + + */ + protected $except = [ + // + ]; +} diff --git a/samooapk/app/Http/Middleware/PreventRequestsDuringMaintenance.php b/samooapk/app/Http/Middleware/PreventRequestsDuringMaintenance.php new file mode 100644 index 0000000..74cbd9a --- /dev/null +++ b/samooapk/app/Http/Middleware/PreventRequestsDuringMaintenance.php @@ -0,0 +1,17 @@ + + */ + protected $except = [ + // + ]; +} diff --git a/samooapk/app/Http/Middleware/RedirectIfAuthenticated.php b/samooapk/app/Http/Middleware/RedirectIfAuthenticated.php new file mode 100644 index 0000000..afc78c4 --- /dev/null +++ b/samooapk/app/Http/Middleware/RedirectIfAuthenticated.php @@ -0,0 +1,30 @@ +check()) { + return redirect(RouteServiceProvider::HOME); + } + } + + return $next($request); + } +} diff --git a/samooapk/app/Http/Middleware/TrimStrings.php b/samooapk/app/Http/Middleware/TrimStrings.php new file mode 100644 index 0000000..88cadca --- /dev/null +++ b/samooapk/app/Http/Middleware/TrimStrings.php @@ -0,0 +1,19 @@ + + */ + protected $except = [ + 'current_password', + 'password', + 'password_confirmation', + ]; +} diff --git a/samooapk/app/Http/Middleware/TrustHosts.php b/samooapk/app/Http/Middleware/TrustHosts.php new file mode 100644 index 0000000..c9c58bd --- /dev/null +++ b/samooapk/app/Http/Middleware/TrustHosts.php @@ -0,0 +1,20 @@ + + */ + public function hosts(): array + { + return [ + $this->allSubdomainsOfApplicationUrl(), + ]; + } +} diff --git a/samooapk/app/Http/Middleware/TrustProxies.php b/samooapk/app/Http/Middleware/TrustProxies.php new file mode 100644 index 0000000..3391630 --- /dev/null +++ b/samooapk/app/Http/Middleware/TrustProxies.php @@ -0,0 +1,28 @@ +|string|null + */ + protected $proxies; + + /** + * The headers that should be used to detect proxies. + * + * @var int + */ + protected $headers = + Request::HEADER_X_FORWARDED_FOR | + Request::HEADER_X_FORWARDED_HOST | + Request::HEADER_X_FORWARDED_PORT | + Request::HEADER_X_FORWARDED_PROTO | + Request::HEADER_X_FORWARDED_AWS_ELB; +} diff --git a/samooapk/app/Http/Middleware/ValidateSignature.php b/samooapk/app/Http/Middleware/ValidateSignature.php new file mode 100644 index 0000000..093bf64 --- /dev/null +++ b/samooapk/app/Http/Middleware/ValidateSignature.php @@ -0,0 +1,22 @@ + + */ + protected $except = [ + // 'fbclid', + // 'utm_campaign', + // 'utm_content', + // 'utm_medium', + // 'utm_source', + // 'utm_term', + ]; +} diff --git a/samooapk/app/Http/Middleware/VerifyCsrfToken.php b/samooapk/app/Http/Middleware/VerifyCsrfToken.php new file mode 100644 index 0000000..9e86521 --- /dev/null +++ b/samooapk/app/Http/Middleware/VerifyCsrfToken.php @@ -0,0 +1,17 @@ + + */ + protected $except = [ + // + ]; +} diff --git a/samooapk/app/Http/Requests/Auth/LoginRequest.php b/samooapk/app/Http/Requests/Auth/LoginRequest.php new file mode 100644 index 0000000..2b92f65 --- /dev/null +++ b/samooapk/app/Http/Requests/Auth/LoginRequest.php @@ -0,0 +1,85 @@ + + */ + public function rules(): array + { + return [ + 'email' => ['required', 'string', 'email'], + 'password' => ['required', 'string'], + ]; + } + + /** + * Attempt to authenticate the request's credentials. + * + * @throws \Illuminate\Validation\ValidationException + */ + public function authenticate(): void + { + $this->ensureIsNotRateLimited(); + + if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { + RateLimiter::hit($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => trans('auth.failed'), + ]); + } + + RateLimiter::clear($this->throttleKey()); + } + + /** + * Ensure the login request is not rate limited. + * + * @throws \Illuminate\Validation\ValidationException + */ + public function ensureIsNotRateLimited(): void + { + if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { + return; + } + + event(new Lockout($this)); + + $seconds = RateLimiter::availableIn($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => trans('auth.throttle', [ + 'seconds' => $seconds, + 'minutes' => ceil($seconds / 60), + ]), + ]); + } + + /** + * Get the rate limiting throttle key for the request. + */ + public function throttleKey(): string + { + return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip()); + } +} diff --git a/samooapk/app/Http/Requests/ProfileUpdateRequest.php b/samooapk/app/Http/Requests/ProfileUpdateRequest.php new file mode 100644 index 0000000..93b0022 --- /dev/null +++ b/samooapk/app/Http/Requests/ProfileUpdateRequest.php @@ -0,0 +1,23 @@ + + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', Rule::unique(User::class)->ignore($this->user()->id)], + ]; + } +} diff --git a/samooapk/app/Models/Absensi.php b/samooapk/app/Models/Absensi.php new file mode 100644 index 0000000..68c5d2c --- /dev/null +++ b/samooapk/app/Models/Absensi.php @@ -0,0 +1,341 @@ + 'date', + 'jam_masuk' => 'datetime', + 'jam_keluar' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime' + ]; + + /** + * Status absensi yang valid + */ + const STATUS_HADIR = 'hadir'; + const STATUS_SAKIT = 'sakit'; + const STATUS_IZIN = 'izin'; + + /** + * Array status yang tersedia + */ + public static function getStatusOptions() + { + return [ + self::STATUS_HADIR => 'Hadir', + self::STATUS_SAKIT => 'Sakit', + self::STATUS_IZIN => 'Izin' + ]; + } + + /** + * Relasi ke model Teknisi + * Satu absensi dimiliki oleh satu teknisi + */ + public function teknisi() + { + return $this->belongsTo(Teknisi::class, 'id_teknisi', 'id_teknisi'); + } + + /** + * Scope untuk filter berdasarkan tanggal + */ + public function scopeFilterByDate($query, $tanggal) + { + if ($tanggal) { + return $query->whereDate('tanggal', $tanggal); + } + return $query; + } + + /** + * Scope untuk filter berdasarkan status + */ + public function scopeFilterByStatus($query, $status) + { + if ($status) { + return $query->where('status', $status); + } + return $query; + } + + /** + * Scope untuk filter berdasarkan teknisi + */ + public function scopeFilterByTeknisi($query, $teknisiId) + { + if ($teknisiId) { + return $query->where('id_teknisi', $teknisiId); + } + return $query; + } + + /** + * Scope untuk filter berdasarkan bulan dan tahun + */ + public function scopeFilterByMonth($query, $bulan, $tahun = null) + { + if (!$tahun) { + $tahun = date('Y'); + } + + return $query->whereMonth('tanggal', $bulan) + ->whereYear('tanggal', $tahun); + } + + /** + * Scope untuk data absensi hari ini + */ + public function scopeToday($query) + { + return $query->whereDate('tanggal', Carbon::today()); + } + + /** + * Scope untuk data absensi minggu ini + */ + public function scopeThisWeek($query) + { + return $query->whereBetween('tanggal', [ + Carbon::now()->startOfWeek(), + Carbon::now()->endOfWeek() + ]); + } + + /** + * Scope untuk data absensi bulan ini + */ + public function scopeThisMonth($query) + { + return $query->whereMonth('tanggal', Carbon::now()->month) + ->whereYear('tanggal', Carbon::now()->year); + } + + /** + * Accessor untuk format tanggal Indonesia + */ + public function getTanggalFormattedAttribute() + { + return Carbon::parse($this->tanggal)->format('d/m/Y'); + } + + /** + * Accessor untuk format jam masuk + */ + public function getJamMasukFormattedAttribute() + { + return $this->jam_masuk ? Carbon::parse($this->jam_masuk)->format('H:i') : '-'; + } + + /** + * Accessor untuk format jam keluar + */ + public function getJamKeluarFormattedAttribute() + { + return $this->jam_keluar ? Carbon::parse($this->jam_keluar)->format('H:i') : '-'; + } + + /** + * Accessor untuk nama status dengan format title case + */ + public function getStatusFormattedAttribute() + { + return ucfirst($this->status); + } + + /** + * Accessor untuk URL foto absen masuk + */ + public function getFotoAbsenMasukUrlAttribute() + { + return $this->foto_absen_masuk ? asset('storage/' . $this->foto_absen_masuk) : null; + } + + /** + * Accessor untuk URL foto absen keluar + */ + public function getFotoAbsenKeluarUrlAttribute() + { + return $this->foto_absen_keluar ? asset('storage/' . $this->foto_absen_keluar) : null; + } + + /** + * Accessor untuk menghitung durasi kerja (dalam menit) + */ + public function getDurasiKerjaAttribute() + { + if ($this->jam_masuk && $this->jam_keluar) { + $masuk = Carbon::parse($this->jam_masuk); + $keluar = Carbon::parse($this->jam_keluar); + return $keluar->diffInMinutes($masuk); + } + return 0; + } + + /** + * Accessor untuk durasi kerja dalam format jam:menit + */ + public function getDurasiKerjaFormattedAttribute() + { + $durasi = $this->durasi_kerja; + if ($durasi > 0) { + $jam = floor($durasi / 60); + $menit = $durasi % 60; + return sprintf('%02d:%02d', $jam, $menit); + } + return '00:00'; + } + + /** + * Accessor untuk menentukan apakah terlambat (asumsi jam masuk normal 08:00) + */ + /** + * Accessor untuk kategori kerja (Kerja Biasa vs Lembur) + */ + public function getKategoriKerjaAttribute() + { + if ($this->jam_masuk) { + $jamMasuk = Carbon::parse($this->jam_masuk); + $start = Carbon::parse('07:00'); + $end = Carbon::parse('18:00'); + + if ($jamMasuk->between($start, $end)) { + return 'Kerja Biasa'; + } + return 'Kerja Urgent'; + } + return '-'; + } + + /** + * Label warna untuk kategori kerja + */ + public function getKategoriBadgeClassAttribute() + { + return $this->kategori_kerja === 'Kerja Biasa' ? 'badge-success' : 'badge-warning'; + } + + /** + * Logika terlambat ditiadakan sesuai permintaan user + */ + public function getIsTerlambatAttribute() + { + return false; + } + + /** + * Accessor untuk CSS class badge berdasarkan status + */ + public function getStatusBadgeClassAttribute() + { + $classes = [ + self::STATUS_HADIR => 'badge-success', + self::STATUS_SAKIT => 'badge-warning', + self::STATUS_IZIN => 'badge-info' + ]; + + return $classes[$this->status] ?? 'badge-secondary'; + } + + /** + * Method untuk mengecek apakah absensi sudah lengkap (ada jam masuk dan keluar) + */ + public function isComplete() + { + return !empty($this->jam_masuk) && !empty($this->jam_keluar); + } + + /** + * Method untuk mengecek apakah sudah absen masuk + */ + public function hasAbsenMasuk() + { + return !empty($this->jam_masuk); + } + + /** + * Method untuk mengecek apakah sudah absen keluar + */ + public function hasAbsenKeluar() + { + return !empty($this->jam_keluar); + } + + /** + * Static method untuk mendapatkan statistik absensi berdasarkan periode + */ + public static function getStatistik($startDate = null, $endDate = null) + { + $query = self::query(); + + if ($startDate && $endDate) { + $query->whereBetween('tanggal', [$startDate, $endDate]); + } + + return [ + 'total' => $query->count(), + 'hadir' => $query->where('status', self::STATUS_HADIR)->count(), + 'sakit' => $query->where('status', self::STATUS_SAKIT)->count(), + 'izin' => $query->where('status', self::STATUS_IZIN)->count(), + ]; + } + + /** + * Boot method untuk event model + */ + protected static function boot() + { + parent::boot(); + + // Event ketika model akan dihapus + static::deleting(function ($absensi) { + // Hapus file foto jika ada + if ($absensi->foto_absen_masuk && Storage::disk('public')->exists($absensi->foto_absen_masuk)) { + Storage::disk('public')->delete($absensi->foto_absen_masuk); + } + + if ($absensi->foto_absen_keluar && Storage::disk('public')->exists($absensi->foto_absen_keluar)) { + Storage::disk('public')->delete($absensi->foto_absen_keluar); + } + }); + } +} \ No newline at end of file diff --git a/samooapk/app/Models/AkunTeknisi.php b/samooapk/app/Models/AkunTeknisi.php new file mode 100644 index 0000000..3286746 --- /dev/null +++ b/samooapk/app/Models/AkunTeknisi.php @@ -0,0 +1,89 @@ + + */ + protected $fillable = [ + 'id_teknisi', + 'username', + 'password', + 'password_plain', + 'status', + ]; + + /** + * Atribut yang harus disembunyikan (hidden) dari array atau JSON. + * + * @var array + */ + protected $hidden = [ + 'password', + ]; + + /** + * Atribut yang harus dikonversi ke tipe data tertentu. + * + * @var array + */ + protected $casts = [ + 'password' => 'hashed', + ]; + + /** + * Relasi ke model Teknisi. + * Sebuah AkunTeknisi dimiliki oleh satu Teknisi. + * + * @return BelongsTo + */ + public function teknisi(): BelongsTo + { + return $this->belongsTo(Teknisi::class, 'id_teknisi', 'id_teknisi'); + } + + /** + * Get the identifier that will be stored in the subject claim of the JWT. + * + * @return mixed + */ + public function getJWTIdentifier() + { + return $this->getKey(); + } + + /** + * Return a key value array, containing any custom claims to be added to the JWT. + * + * @return array + */ + public function getJWTCustomClaims() + { + return []; + } +} \ No newline at end of file diff --git a/samooapk/app/Models/DashboardStat.php b/samooapk/app/Models/DashboardStat.php new file mode 100644 index 0000000..9e7589f --- /dev/null +++ b/samooapk/app/Models/DashboardStat.php @@ -0,0 +1,21 @@ + 0, + 'teknisiAktif' => 0, + 'totalPekerjaan' => 0, + 'penugasanAktif' => 0, + 'chartPenugasan' => [0,0,0,0,0,0,0,0,0,0,0,0], + 'chartSelesai' => [0,0,0,0,0,0,0,0,0,0,0,0], + 'kontrakJatuhTempo' => collect(), + ]); + } +} \ No newline at end of file diff --git a/samooapk/app/Models/DetailPenggajian.php b/samooapk/app/Models/DetailPenggajian.php new file mode 100644 index 0000000..5356f76 --- /dev/null +++ b/samooapk/app/Models/DetailPenggajian.php @@ -0,0 +1,78 @@ + 'decimal:2', + 'jumlah_tim' => 'integer', + 'bagian_ongkos' => 'decimal:2', + 'tanggal_selesai' => 'date', + ]; + + /** + * Relationship with Penggajian + */ + public function penggajian() + { + return $this->belongsTo(Penggajian::class, 'id_penggajian', 'id_penggajian'); + } + + /** + * Relationship with Penugasan + */ + public function penugasan() + { + return $this->belongsTo(Penugasan::class, 'id_penugasan', 'id_penugasan'); + } + + /** + * Boot method to update parent totals + */ + protected static function boot() + { + parent::boot(); + + static::saved(function ($detail) { + $detail->updatePenggajianTotals(); + }); + + static::deleted(function ($detail) { + $detail->updatePenggajianTotals(); + }); + } + + public function updatePenggajianTotals() + { + $penggajian = $this->penggajian; + if ($penggajian) { + $totalOngkos = self::where('id_penggajian', $this->id_penggajian)->sum('bagian_ongkos'); + $biayaMakan = $penggajian->biaya_makan ?? 0; + $potongan = $penggajian->total_potongan ?? $penggajian->total_kasbon ?? 0; + $penggajian->update([ + 'total_ongkos_pekerjaan' => $totalOngkos, + 'gaji_bersih' => ($totalOngkos - $biayaMakan) - $potongan, + ]); + } + } +} \ No newline at end of file diff --git a/samooapk/app/Models/Kasbon.php b/samooapk/app/Models/Kasbon.php new file mode 100644 index 0000000..c33900b --- /dev/null +++ b/samooapk/app/Models/Kasbon.php @@ -0,0 +1,192 @@ +tanggal_kasbon) { + $date = \Carbon\Carbon::parse($kasbon->tanggal_kasbon); + $kasbon->periode_bulan = $date->month; + $kasbon->periode_tahun = $date->year; + } + if (auth()->check()) { + $kasbon->diinput_oleh = auth()->id(); + } + }); + } + + /** + * The table associated with the model. + * + * @var string + */ + protected $table = 'kasbons'; + + /** + * The primary key associated with the table. + * + * @var string + */ + protected $primaryKey = 'id_kasbon'; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'id_teknisi', + 'jumlah_kasbon', + 'tanggal_kasbon', + 'periode_bulan', + 'periode_tahun', + 'keperluan', + 'keterangan_detail', + 'metode_pemberian', + 'diinput_oleh', + 'status', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'jumlah_kasbon' => 'decimal:2', + 'tanggal_kasbon' => 'date', + 'status' => 'string', + 'created_at' => 'timestamp', + 'updated_at' => 'timestamp', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var array + */ + protected $hidden = []; + + /** + * Status constants + */ + const STATUS_LUNAS = 'lunas'; + const STATUS_BELUM_LUNAS = 'belum_lunas'; + + /** + * Get all available status options + * + * @return array + */ + public static function getStatusOptions() + { + return [ + self::STATUS_LUNAS => 'Lunas', + self::STATUS_BELUM_LUNAS => 'Belum Lunas' + ]; + } + + /** + * Scope untuk filter berdasarkan status + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string $status + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeByStatus($query, $status) + { + return $query->where('status', $status); + } + + /** + * Scope untuk kasbon yang belum lunas + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeBelumLunas($query) + { + return $query->where('status', self::STATUS_BELUM_LUNAS); + } + + /** + * Scope untuk kasbon yang sudah lunas + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeLunas($query) + { + return $query->where('status', self::STATUS_LUNAS); + } + + /** + * Accessor untuk format jumlah kasbon dalam rupiah + * + * @return string + */ + public function getJumlahKasbonFormatAttribute() + { + return 'Rp ' . number_format($this->jumlah_kasbon, 0, ',', '.'); + } + + /** + * Accessor untuk format tanggal kasbon + * + * @return string + */ + public function getTanggalKasbonFormatAttribute() + { + return $this->tanggal_kasbon ? $this->tanggal_kasbon->format('d/m/Y') : ''; + } + + /** + * Accessor untuk status dalam bahasa Indonesia + * + * @return string + */ + public function getStatusLabelAttribute() + { + $statusOptions = self::getStatusOptions(); + return $statusOptions[$this->status] ?? $this->status; + } + + /** + * Mutator untuk format jumlah kasbon sebelum disimpan + * + * @param mixed $value + * @return void + */ + public function setJumlahKasbonAttribute($value) + { + // Hapus format rupiah jika ada + $cleanValue = str_replace(['Rp', '.', ',', ' '], '', $value); + $this->attributes['jumlah_kasbon'] = (float) $cleanValue; + } + + /** + * Accessor untuk keterangan (alias keperluan) + */ + public function getKeteranganAttribute() + { + return $this->keperluan; + } + + /** + * Relasi ke Teknisi + */ + public function teknisi() + { + return $this->belongsTo(Teknisi::class, 'id_teknisi', 'id_teknisi'); + } +} \ No newline at end of file diff --git a/samooapk/app/Models/Laporan.php b/samooapk/app/Models/Laporan.php new file mode 100644 index 0000000..7d36e2f --- /dev/null +++ b/samooapk/app/Models/Laporan.php @@ -0,0 +1,228 @@ + DB::table('kasbons')->count(), + 'total_teknisi' => DB::table('teknisis')->count(), + 'total_pekerjaan' => DB::table('penugasans')->count(), + 'total_absensi' => DB::table('absensis') + ->whereMonth('tanggal', date('m')) + ->whereYear('tanggal', date('Y')) + ->count(), + + // Kasbon statistics + 'total_lunas' => DB::table('kasbons') + ->where('status', 'lunas') + ->sum('jumlah_kasbon'), + 'total_belum_lunas' => DB::table('kasbons') + ->where('status', 'belum_lunas') + ->sum('jumlah_kasbon'), + + // Teknisi statistics + 'teknisi_aktif' => DB::table('teknisis') + ->where('status', 'aktif') + ->count(), + 'teknisi_nonaktif' => DB::table('teknisis') + ->where('status', 'nonaktif') + ->count(), + + // Absensi statistics + 'hadir' => DB::table('absensis') + ->where('status', 'hadir') + ->whereMonth('tanggal', date('m')) + ->whereYear('tanggal', date('Y')) + ->count(), + 'izin' => DB::table('absensis') + ->where('status', 'izin') + ->whereMonth('tanggal', date('m')) + ->whereYear('tanggal', date('Y')) + ->count(), + 'sakit' => DB::table('absensis') + ->where('status', 'sakit') + ->whereMonth('tanggal', date('m')) + ->whereYear('tanggal', date('Y')) + ->count(), + 'alpha' => DB::table('absensis') + ->where('status', 'alpha') + ->whereMonth('tanggal', date('m')) + ->whereYear('tanggal', date('Y')) + ->count(), + + // Pekerjaan statistics - using penugasans table + 'selesai' => DB::table('penugasans') + ->where('status_pekerjaan', 'selesai') + ->count(), + 'progress' => DB::table('penugasans') + ->where('status_pekerjaan', 'proses') + ->count(), + 'pending' => DB::table('penugasans') + ->where('status_pekerjaan', 'pending') + ->count(), + ]; + } + + /** + * Get kasbon data with filters + */ + public static function getKasbonData($filters = []) + { + $query = DB::table('kasbons') + ->leftJoin('teknisis', 'kasbons.id_teknisi', '=', 'teknisis.id_teknisi') + ->select( + 'kasbons.*', + 'teknisis.nama as nama_teknisi' + ); + + if (!empty($filters['search'])) { + $query->where(function($q) use ($filters) { + $q->where('teknisis.nama', 'like', '%' . $filters['search'] . '%') + ->orWhere('kasbons.keperluan', 'like', '%' . $filters['search'] . '%'); + }); + } + + if (!empty($filters['tanggal_dari'])) { + $query->whereDate('kasbons.tanggal_kasbon', '>=', $filters['tanggal_dari']); + } + + if (!empty($filters['tanggal_sampai'])) { + $query->whereDate('kasbons.tanggal_kasbon', '<=', $filters['tanggal_sampai']); + } + + if (!empty($filters['status'])) { + $query->where('kasbons.status', $filters['status']); + } + + if (!empty($filters['id_teknisi'])) { + $query->where('kasbons.id_teknisi', $filters['id_teknisi']); + } + + return $query->orderBy('kasbons.tanggal_kasbon', 'desc'); + } + + /** + * Get teknisi data with filters + */ + public static function getTeknisiData($filters = []) + { + // Use raw SQL for groupBy to avoid MySQL strict mode issues + $query = DB::table('teknisis') + ->leftJoin('absensis', function($join) use ($filters) { + $join->on('teknisis.id_teknisi', '=', 'absensis.id_teknisi'); + if (!empty($filters['tanggal_dari'])) { + $join->whereDate('absensis.tanggal', '>=', $filters['tanggal_dari']); + } + if (!empty($filters['tanggal_sampai'])) { + $join->whereDate('absensis.tanggal', '<=', $filters['tanggal_sampai']); + } + }) + ->select( + 'teknisis.id_teknisi', + 'teknisis.nama', + 'teknisis.status', + DB::raw('COUNT(absensis.id_absensi) as total_absensi'), + DB::raw('COUNT(CASE WHEN absensis.status = "hadir" THEN 1 END) as hadir'), + DB::raw('COUNT(CASE WHEN absensis.status = "izin" THEN 1 END) as izin'), + DB::raw('COUNT(CASE WHEN absensis.status = "sakit" THEN 1 END) as sakit'), + DB::raw('COUNT(CASE WHEN absensis.status = "alpha" THEN 1 END) as alpha') + ) + ->groupBy('teknisis.id_teknisi', 'teknisis.nama', 'teknisis.status'); + + if (!empty($filters['search'])) { + $query->where('teknisis.nama', 'like', '%' . $filters['search'] . '%'); + } + + return $query->orderBy('teknisis.nama', 'asc'); + } + + /** + * Get absensi data with filters + */ + public static function getAbsensiData($filters = []) + { + $query = DB::table('absensis') + ->leftJoin('teknisis', 'absensis.id_teknisi', '=', 'teknisis.id_teknisi') + ->select( + 'absensis.*', + 'teknisis.nama as nama_teknisi' + ); + + if (!empty($filters['search'])) { + $query->where('teknisis.nama', 'like', '%' . $filters['search'] . '%'); + } + + if (!empty($filters['tanggal_dari'])) { + $query->whereDate('absensis.tanggal', '>=', $filters['tanggal_dari']); + } + + if (!empty($filters['tanggal_sampai'])) { + $query->whereDate('absensis.tanggal', '<=', $filters['tanggal_sampai']); + } + + if (!empty($filters['status'])) { + $query->where('absensis.status', $filters['status']); + } + + if (!empty($filters['id_teknisi'])) { + $query->where('absensis.id_teknisi', $filters['id_teknisi']); + } + + return $query->orderBy('absensis.tanggal', 'desc'); + } + + /** + * Get pekerjaan data with filters + */ + public static function getPekerjaanData($filters = []) + { + $query = DB::table('penugasans') + ->leftJoin('teknisis', 'penugasans.id_teknisi', '=', 'teknisis.id_teknisi') + ->select( + 'penugasans.*', + 'teknisis.nama as nama_teknisi' + ); + + if (!empty($filters['search'])) { + $query->where(function($q) use ($filters) { + $q->where('teknisis.nama', 'like', '%' . $filters['search'] . '%') + ->orWhere('penugasans.jenis_pekerjaan', 'like', '%' . $filters['search'] . '%') + ->orWhere('penugasans.catatan_admin', 'like', '%' . $filters['search'] . '%'); + }); + } + + if (!empty($filters['tanggal_dari'])) { + $query->whereDate('penugasans.tanggal_mulai', '>=', $filters['tanggal_dari']); + } + + if (!empty($filters['tanggal_sampai'])) { + $query->whereDate('penugasans.tanggal_diselesaikan', '<=', $filters['tanggal_sampai']); + } + + if (!empty($filters['status'])) { + $query->where('penugasans.status_pekerjaan', $filters['status']); + } + + if (!empty($filters['id_teknisi'])) { + $query->where('penugasans.id_teknisi', $filters['id_teknisi']); + } + + return $query->orderBy('penugasans.created_at', 'desc'); + } +} \ No newline at end of file diff --git a/samooapk/app/Models/Penggajian.php b/samooapk/app/Models/Penggajian.php new file mode 100644 index 0000000..9d8ddf3 --- /dev/null +++ b/samooapk/app/Models/Penggajian.php @@ -0,0 +1,383 @@ + 'Januari', 2 => 'Februari', 3 => 'Maret', 4 => 'April', + 5 => 'Mei', 6 => 'Juni', 7 => 'Juli', 8 => 'Agustus', + 9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Desember' + ]; + return $bulan[(int)$angka] ?? 'N/A'; + } + + /** + * The table associated with the model. + */ + protected $table = 'penggajians'; + + /** + * The primary key for the model. + */ + protected $primaryKey = 'id_penggajian'; + + /** + * The attributes that are mass assignable. + */ + protected $fillable = [ + 'id_teknisi', + 'periode_bulan', + 'periode_tahun', + 'tanggal_penggajian', + 'total_ongkos_pekerjaan', + 'jumlah_penugasan_selesai', + 'jumlah_hari_kerja', + 'total_kasbon', + 'biaya_makan', + 'total_potongan', + 'gaji_bersih', + 'status_pembayaran', + 'metode_pembayaran', + 'tanggal_dibayar', + 'bukti_pembayaran', + 'catatan', + ]; + + /** + * The attributes that should be cast. + */ + protected $casts = [ + 'tanggal_penggajian' => 'date', + 'jumlah_hari_kerja' => 'integer', + 'total_ongkos_pekerjaan' => 'decimal:2', + 'total_kasbon' => 'decimal:2', + 'biaya_makan' => 'decimal:2', + 'total_potongan' => 'decimal:2', + 'gaji_bersih' => 'decimal:2', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + /** + * The attributes that should be hidden for arrays. + */ + protected $hidden = [ + 'deleted_at', + ]; + + /** + * Status pembayaran constants + */ + const STATUS_BELUM_BAYAR = 'belum_bayar'; + const STATUS_SUDAH_BAYAR = 'sudah_bayar'; + + /** + * Get all available status pembayaran + */ + public static function getStatusPembayaran() + { + return [ + self::STATUS_BELUM_BAYAR => 'Belum Bayar', + self::STATUS_SUDAH_BAYAR => 'Sudah Bayar', + ]; + } + + /** + * Relationship with Teknisi + */ + public function teknisi() + { + return $this->belongsTo(Teknisi::class, 'id_teknisi', 'id_teknisi'); + } + + /** + * Relationship with DetailPenggajian + */ + public function detailPenggajian() + { + return $this->hasMany(DetailPenggajian::class, 'id_penggajian', 'id_penggajian'); + } + + /** + * Scope for filtering by periode + */ + public function scopeByPeriode($query, $bulan = null, $tahun = null) + { + if ($bulan) { + $query->where('periode_bulan', $bulan); + } + if ($tahun) { + $query->where('periode_tahun', $tahun); + } + return $query; + } + + /** + * Scope for filtering by status pembayaran + */ + public function scopeByStatus($query, $status) + { + return $query->where('status_pembayaran', $status); + } + + /** + * Scope for belum bayar + */ + public function scopeBelumBayar($query) + { + return $query->where('status_pembayaran', self::STATUS_BELUM_BAYAR); + } + + /** + * Scope for sudah bayar + */ + public function scopeSudahBayar($query) + { + return $query->where('status_pembayaran', self::STATUS_SUDAH_BAYAR); + } + + /** + * Scope for current month + */ + public function scopeCurrentMonth($query) + { + return $query->where('periode_bulan', date('n')) + ->where('periode_tahun', date('Y')); + } + + /** + * Scope for latest periode + */ + public function scopeLatestPeriode($query) + { + return $query->orderBy('periode_tahun', 'desc') + ->orderBy('periode_bulan', 'desc'); + } + + /** + * Get formatted periode + */ + public function getFormattedPeriodeAttribute() + { + $bulan = Carbon::create()->month($this->periode_bulan)->format('F'); + return $bulan . ' ' . $this->periode_tahun; + } + + /** + * Get periode short format + */ + public function getPeriodeShortAttribute() + { + $bulan = Carbon::create()->month($this->periode_bulan)->format('M'); + return $bulan . ' ' . $this->periode_tahun; + } + + /** + * Get status pembayaran label + */ + public function getStatusLabelAttribute() + { + $statuses = self::getStatusPembayaran(); + return $statuses[$this->status_pembayaran] ?? $this->status_pembayaran; + } + + /** + * Get status badge class + */ + public function getStatusBadgeClassAttribute() + { + switch ($this->status_pembayaran) { + case self::STATUS_SUDAH_BAYAR: + return 'bg-success'; + case self::STATUS_BELUM_BAYAR: + default: + return 'bg-warning'; + } + } + + /** + * Check if already paid + */ + public function isPaid() + { + return $this->status_pembayaran === self::STATUS_SUDAH_BAYAR; + } + + /** + * Check if not paid yet + */ + public function isUnpaid() + { + return $this->status_pembayaran === self::STATUS_BELUM_BAYAR; + } + + /** + * Mark as paid + */ + public function markAsPaid() + { + $this->update([ + 'status_pembayaran' => self::STATUS_SUDAH_BAYAR + ]); + } + + /** + * Mark as unpaid + */ + public function markAsUnpaid() + { + $this->update([ + 'status_pembayaran' => self::STATUS_BELUM_BAYAR + ]); + } + + /** + * Calculate total from detail penggajian + */ + public function calculateTotals() + { + $details = $this->detailPenggajian; + + $totals = [ + 'subtotal' => $details->sum('subtotal'), + 'total_unit' => $details->sum('jumlah_unit'), + ]; + + return $totals; + } + + /** + * Get formatted currency attributes + */ + public function getFormattedGajiKotorAttribute() + { + return 'Rp ' . number_format($this->total_ongkos_pekerjaan, 0, ',', '.'); + } + + public function getFormattedBiayaMakanAttribute() + { + return 'Rp ' . number_format($this->biaya_makan, 0, ',', '.'); + } + + public function getFormattedTotalKasbonAttribute() + { + return 'Rp ' . number_format($this->total_kasbon, 0, ',', '.'); + } + + public function getFormattedGajiBersihAttribute() + { + return 'Rp ' . number_format($this->gaji_bersih, 0, ',', '.'); + } + + /** + * Get potongan (total kasbon) + */ + public function getPotonganAttribute() + { + return $this->total_kasbon; + } + + /** + * Get formatted potongan + */ + public function getFormattedPotonganAttribute() + { + return 'Rp ' . number_format($this->potongan, 0, ',', '.'); + } + + /** + * Static method to get summary by periode + */ + public static function getSummaryByPeriode($bulan = null, $tahun = null, $status = null) + { + $query = self::with('teknisi'); + + if ($bulan) { + $query->where('periode_bulan', $bulan); + } + if ($tahun) { + $query->where('periode_tahun', $tahun); + } + if ($status) { + $query->where('status_pembayaran', $status); + } + + $data = $query->get(); + + return [ + 'total_teknisi' => $data->count(), + 'total_gaji' => $data->sum('total_ongkos_pekerjaan'), + 'total_kasbon' => $data->sum('total_kasbon'), + 'total_biaya_makan' => $data->sum('biaya_makan'), + 'gaji_bersih' => $data->sum('gaji_bersih'), + 'belum_bayar' => $data->where('status_pembayaran', self::STATUS_BELUM_BAYAR)->count(), + 'sudah_bayar' => $data->where('status_pembayaran', self::STATUS_SUDAH_BAYAR)->count(), + ]; + } + + /** + * Static method to check if periode already exists for teknisi + */ + public static function isPeriodeExists($idTeknisi, $bulan, $tahun) + { + return self::where('id_teknisi', $idTeknisi) + ->where('periode_bulan', $bulan) + ->where('periode_tahun', $tahun) + ->exists(); + } + + /** + * Static method to get latest periode + */ + public static function getLatestPeriode() + { + $latest = self::orderBy('periode_tahun', 'desc') + ->orderBy('periode_bulan', 'desc') + ->first(); + + if (!$latest) { + return [ + 'bulan' => date('n'), + 'tahun' => date('Y') + ]; + } + + return [ + 'bulan' => $latest->periode_bulan, + 'tahun' => $latest->periode_tahun + ]; + } + + /** + * Boot method + */ + protected static function boot() + { + parent::boot(); + + // Auto calculate gaji_bersih before saving + static::saving(function ($penggajian) { + $ongkos = $penggajian->total_ongkos_pekerjaan ?? 0; + $makan = $penggajian->biaya_makan ?? 0; + $potongan = $penggajian->total_potongan ?? 0; + + // Gaji Bersih = Ongkos - Uang Makan - Potongan Kasbon + // (Uang makan sekarang dihitung sebagai potongan) + $penggajian->gaji_bersih = $ongkos - $makan - $potongan; + }); + } +} \ No newline at end of file diff --git a/samooapk/app/Models/Penugasan.php b/samooapk/app/Models/Penugasan.php new file mode 100644 index 0000000..6e4ab3b --- /dev/null +++ b/samooapk/app/Models/Penugasan.php @@ -0,0 +1,202 @@ + 'date', + 'tanggal_mulai' => 'datetime', + 'tanggal_diselesaikan' => 'datetime', + 'tanggal_garansi_mulai' => 'date', + 'tanggal_garansi_selesai' => 'date', + 'jarak_meter' => 'decimal:2', + 'total_nilai_pekerjaan' => 'decimal:2', + 'pakai_pipa_besi' => 'boolean', + ]; + + // =================================== + // RELATIONSHIPS + // =================================== + + public function teknisi() + { + return $this->belongsTo(Teknisi::class, 'id_teknisi', 'id_teknisi'); + } + + public function teknisiPertama() + { + return $this->teknisi(); + } + + public function tarif() + { + return $this->belongsTo(TarifPekerjaan::class, 'id_tarif', 'id_tarif'); + } + + public function timTeknisi() + { + return $this->hasMany(TimTeknisiPenugasan::class, 'id_penugasan', 'id_penugasan'); + } + + public function items() + { + return $this->hasMany(PenugasanItem::class, 'id_penugasan', 'id_penugasan'); + } + + // =================================== + // ACCESSOR - FOTO URL + // =================================== + + public function getFotoSuratUrlAttribute(): ?string +{ + if (!$this->foto_surat) return null; + return 'https://ta.myhost.id/E31230906/laravel/public/storage/' . $this->foto_surat; +} + +public function getFotoSebelumUrlAttribute(): ?string +{ + if (!$this->foto_sebelum) return null; + return 'https://ta.myhost.id/E31230906/laravel/public/storage/' . $this->foto_sebelum; +} + +public function getFotoSesudahUrlAttribute(): ?string +{ + if (!$this->foto_sesudah) return null; + return 'https://ta.myhost.id/E31230906/laravel/public/storage/' . $this->foto_sesudah; +} + + // =================================== + // HELPER - TIM + // =================================== + + public function getAllTimTeknisi() + { + return $this->timTeknisi()->with('teknisi')->get(); + } + + public function countTotalTim(): int + { + return $this->timTeknisi()->count(); + } + + public function countTimHadir(): int + { + return $this->timTeknisi()->where('status_kehadiran', 'hadir')->count(); + } + + public function getOngkosPerOrang(): float + { + $jumlahHadir = $this->countTimHadir(); + if ($jumlahHadir === 0 || !$this->total_nilai_pekerjaan) return 0; + return $this->total_nilai_pekerjaan / $jumlahHadir; + } + + // =================================== + // HELPER - GARANSI + // =================================== + + public function setGaransiMeteranAir($tanggalMulai = null) + { + $mulai = $tanggalMulai ? Carbon::parse($tanggalMulai) : Carbon::now(); + $this->tanggal_garansi_mulai = $mulai; + $this->tanggal_garansi_selesai = $mulai->copy()->addMonths(3); + $this->catatan_garansi = 'Garansi 3 bulan untuk pemasangan meteran air (SR)'; + } + + public function isGaransiAktif(): bool + { + if (!$this->tanggal_garansi_mulai || !$this->tanggal_garansi_selesai) return false; + return now()->between($this->tanggal_garansi_mulai, $this->tanggal_garansi_selesai); + } + + public function getSisaHariGaransi(): ?int + { + if (!$this->tanggal_garansi_selesai) return null; + $sisa = now()->diffInDays($this->tanggal_garansi_selesai, false); + return $sisa > 0 ? (int)$sisa : 0; + } + + public function scopeGaransiAktif($query) + { + return $query->whereNotNull('tanggal_garansi_mulai') + ->whereNotNull('tanggal_garansi_selesai') + ->where('tanggal_garansi_selesai', '>=', now()); + } + + // =================================== + // HELPER - STATUS + // =================================== + + public function isDetailLengkap(): bool + { + return !is_null($this->jenis_pekerjaan); + } + + public function isSelesai(): bool + { + return $this->status_pekerjaan === 'selesai'; + } + + public function getLabelJenisPekerjaanAttribute(): string + { + $labels = [ + 'sr' => 'SR (Sambungan Rumah)', + 'pengembangan_jaringan_pipa' => 'Pengembangan Jaringan Pipa', + 'pengangkatan' => 'Pengangkatan', + 'pemasangan_gate_valve' => 'Pemasangan Gate Valve', + 'gali_urug' => 'Gali Urug', + 'perbaikan_jaringan_pipa' => 'Perbaikan Jaringan Pipa', + 'pengecatan_pipa_besi' => 'Pengecatan Pipa Besi', + 'penyempurnaan_jaringan_pipa' => 'Penyempurnaan Jaringan Pipa', + ]; + return $labels[$this->jenis_pekerjaan] ?? '-'; + } +} \ No newline at end of file diff --git a/samooapk/app/Models/PenugasanItem.php b/samooapk/app/Models/PenugasanItem.php new file mode 100644 index 0000000..267882d --- /dev/null +++ b/samooapk/app/Models/PenugasanItem.php @@ -0,0 +1,50 @@ +belongsTo(Penugasan::class, 'id_penugasan', 'id_penugasan'); + } + + public function tarif() + { + return $this->belongsTo(TarifPekerjaan::class, 'id_tarif', 'id_tarif'); + } + + public function getLabelJenisPekerjaanAttribute(): string + { + $labels = [ + 'sr' => 'SR (Sambungan Rumah)', + 'pengembangan_jaringan_pipa' => 'Pengembangan Jaringan Pipa', + 'pengangkatan' => 'Pengangkatan', + 'pemasangan_gate_valve' => 'Pemasangan Gate Valve', + 'gali_urug' => 'Gali Urug', + 'perbaikan_jaringan_pipa' => 'Perbaikan Jaringan Pipa', + 'pengecatan_pipa_besi' => 'Pengecatan Pipa Besi', + 'penyempurnaan_jaringan_pipa' => 'Penyempurnaan Jaringan Pipa', + ]; + return $labels[$this->jenis_pekerjaan] ?? $this->jenis_pekerjaan; + } +} diff --git a/samooapk/app/Models/ProgresKerja.php b/samooapk/app/Models/ProgresKerja.php new file mode 100644 index 0000000..8d0da01 --- /dev/null +++ b/samooapk/app/Models/ProgresKerja.php @@ -0,0 +1,254 @@ + 'datetime', + 'persentase_pekerjaan' => 'integer', + ]; + + // Status progres options + public static $statusProgres = [ + 'belum_mulai' => 'Belum Mulai', + 'dalam_progres' => 'Dalam Progres', + 'terhenti' => 'Terhenti', + 'selesai' => 'Selesai', + 'ditunda' => 'Ditunda' + ]; + + // Boot method untuk auto update tanggal_update + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + $model->tanggal_update = now(); + }); + + static::updating(function ($model) { + $model->tanggal_update = now(); + }); + } + + // Relationships + public function teknisi() + { + return $this->belongsTo(Teknisi::class, 'id_teknisi', 'id_teknisi'); + } + + public function penugasan() + { + return $this->belongsTo(Penugasan::class, 'id_penugasan', 'id_penugasan'); + } + + // Scopes + public function scopeByStatus($query, $status) + { + return $query->where('status_progres', $status); + } + + public function scopeByTeknisi($query, $teknisiId) + { + return $query->where('id_teknisi', $teknisiId); + } + + public function scopeByDateRange($query, $startDate = null, $endDate = null) + { + if ($startDate) { + $query->whereDate('tanggal_update', '>=', $startDate); + } + + if ($endDate) { + $query->whereDate('tanggal_update', '<=', $endDate); + } + + return $query; + } + + // Accessors + public function getStatusFormattedAttribute() + { + return self::$statusProgres[$this->status_progres] ?? 'Unknown'; + } + + public function getStatusBadgeClassAttribute() + { + $badgeClasses = [ + 'belum_mulai' => 'badge-secondary', + 'dalam_progres' => 'badge-primary', + 'terhenti' => 'badge-danger', + 'selesai' => 'badge-success', + 'ditunda' => 'badge-warning' + ]; + + return $badgeClasses[$this->status_progres] ?? 'badge-secondary'; + } + + public function getTanggalUpdateFormattedAttribute() + { + return $this->tanggal_update ? $this->tanggal_update->format('d/m/Y H:i') : '-'; + } + + public function getHasFotoAttribute() + { + return !empty($this->foto_progress); + } + + public function getFotoProgressUrlAttribute() + { + if ($this->foto_progress) { + return asset('storage/progress_photos/' . $this->foto_progress); + } + return null; + } + + public function getDaysSinceUpdateAttribute() + { + if ($this->tanggal_update) { + return $this->tanggal_update->diffInDays(now()); + } + return 0; + } + + // Methods + public function autoUpdateStatus() + { + $percentage = $this->persentase_pekerjaan; + + if ($percentage == 0) { + $newStatus = 'belum_mulai'; + } elseif ($percentage > 0 && $percentage < 100) { + $newStatus = 'dalam_progres'; + } elseif ($percentage == 100) { + $newStatus = 'selesai'; + } else { + return; // No change needed + } + + // Only update if status is different + if ($this->status_progres !== $newStatus) { + $this->update(['status_progres' => $newStatus]); + } + } + + public function canTransitionTo($newStatus) + { + $allowedTransitions = [ + 'belum_mulai' => ['dalam_progres', 'ditunda'], + 'dalam_progres' => ['terhenti', 'selesai', 'ditunda'], + 'terhenti' => ['dalam_progres', 'ditunda'], + 'selesai' => [], // Cannot transition from completed + 'ditunda' => ['belum_mulai', 'dalam_progres'] + ]; + + $currentStatus = $this->status_progres; + + return in_array($newStatus, $allowedTransitions[$currentStatus] ?? []); + } + + // Static methods + public static function getByTeknisi($teknisiId) + { + return self::with(['teknisi', 'penugasan']) + ->where('id_teknisi', $teknisiId) + ->orderBy('tanggal_update', 'desc') + ->get(); + } + + public static function getRecentUpdates($limit = 10) + { + return self::with(['teknisi', 'penugasan']) + ->orderBy('tanggal_update', 'desc') + ->limit($limit) + ->get(); + } + + public static function getStatistics() + { + $total = self::count(); + $byStatus = self::selectRaw('status_progres, COUNT(*) as count') + ->groupBy('status_progres') + ->pluck('count', 'status_progres') + ->toArray(); + + $statistics = [ + 'total' => $total, + 'belum_mulai' => $byStatus['belum_mulai'] ?? 0, + 'dalam_progres' => $byStatus['dalam_progres'] ?? 0, + 'terhenti' => $byStatus['terhenti'] ?? 0, + 'selesai' => $byStatus['selesai'] ?? 0, + 'ditunda' => $byStatus['ditunda'] ?? 0, + ]; + + // Add percentages + if ($total > 0) { + $statistics['completion_rate'] = round(($statistics['selesai'] / $total) * 100, 1); + $statistics['in_progress_rate'] = round(($statistics['dalam_progres'] / $total) * 100, 1); + } else { + $statistics['completion_rate'] = 0; + $statistics['in_progress_rate'] = 0; + } + + return $statistics; + } + + public static function getOverdueProgress($days = 7) + { + return self::with(['teknisi', 'penugasan']) + ->whereIn('status_progres', ['dalam_progres', 'terhenti']) + ->where('tanggal_update', '<', now()->subDays($days)) + ->orderBy('tanggal_update', 'asc') + ->get(); + } + + public static function getProgressSummaryByTeknisi() + { + return self::with('teknisi') + ->selectRaw('id_teknisi, status_progres, COUNT(*) as count') + ->groupBy('id_teknisi', 'status_progres') + ->get() + ->groupBy('id_teknisi') + ->map(function ($items) { + $teknisi = $items->first()->teknisi; + $summary = [ + 'teknisi_id' => $teknisi->id_teknisi, + 'teknisi_nama' => $teknisi->nama, + 'total' => $items->sum('count'), + 'belum_mulai' => 0, + 'dalam_progres' => 0, + 'terhenti' => 0, + 'selesai' => 0, + 'ditunda' => 0, + ]; + + foreach ($items as $item) { + $summary[$item->status_progres] = $item->count; + } + + return $summary; + }) + ->values(); + } +} \ No newline at end of file diff --git a/samooapk/app/Models/TarifPekerjaan.php b/samooapk/app/Models/TarifPekerjaan.php new file mode 100644 index 0000000..01a4e72 --- /dev/null +++ b/samooapk/app/Models/TarifPekerjaan.php @@ -0,0 +1,61 @@ + 'decimal:2', + 'tarif_per_meter' => 'decimal:2', + 'pakai_pipa_besi' => 'boolean', + 'ada_garansi' => 'boolean', + 'is_active' => 'boolean', + ]; + + // Accessor: ambil harga (tarif_per_unit atau tarif_per_meter) + public function getHargaAttribute(): float + { + return (float) ($this->tarif_per_unit ?? $this->tarif_per_meter ?? 0); + } + + // Relationship + public function penugasans() + { + return $this->hasMany(Penugasan::class, 'id_tarif', 'id_tarif'); + } + + // Scope untuk filter aktif + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + // Scope untuk filter by kategori + public function scopeKategori($query, $kategori) + { + return $query->where('kategori', $kategori); + } +} \ No newline at end of file diff --git a/samooapk/app/Models/Teknisi.php b/samooapk/app/Models/Teknisi.php new file mode 100644 index 0000000..62efaf5 --- /dev/null +++ b/samooapk/app/Models/Teknisi.php @@ -0,0 +1,134 @@ + + */ + protected $fillable = [ + 'nama', + 'tanggal_lahir', + 'alamat', + 'email', + 'no_telephone', + 'tanggal_masuk', + 'status', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'tanggal_lahir' => 'date', + 'tanggal_masuk' => 'date', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var array + */ + protected $hidden = []; + + /** + * Append computed attributes for API responses. + * + * @var array + */ + protected $appends = ['has_unpaid_kasbon', 'unpaid_kasbon_amount']; + + // ─── Relationships ──────────────────────────────────────────── + + /** + * Relationship to Kasbon entries. + */ + public function kasbons() + { + return $this->hasMany(\App\Models\Kasbon::class, 'id_teknisi', 'id_teknisi'); + } + + // ─── Scopes ─────────────────────────────────────────────────── + + /** + * Scope a query to only include active teknisi. + */ + public function scopeAktif($query) + { + return $query->where('status', 'aktif'); + } + + /** + * Scope a query to only include inactive teknisi. + */ + public function scopeTidakAktif($query) + { + return $query->where('status', 'tidak_aktif'); + } + + // ─── Helper Methods ─────────────────────────────────────────── + + /** + * Check if the teknisi has any unpaid (belum_lunas) kasbon. + */ + public function hasUnpaidKasbon(): bool + { + return \App\Models\Kasbon::where('id_teknisi', $this->id_teknisi) + ->where('status', 'belum_lunas') + ->exists(); + } + + /** + * Get total amount of unpaid kasbon. + */ + public function unpaidKasbonAmount(): float + { + return (float) \App\Models\Kasbon::where('id_teknisi', $this->id_teknisi) + ->where('status', 'belum_lunas') + ->sum('jumlah_kasbon'); + } + + // ─── Accessors ──────────────────────────────────────────────── + + /** + * Accessor for has_unpaid_kasbon attribute. + */ + public function getHasUnpaidKasbonAttribute(): bool + { + return $this->hasUnpaidKasbon(); + } + + /** + * Accessor for unpaid_kasbon_amount attribute. + */ + public function getUnpaidKasbonAmountAttribute(): float + { + return $this->unpaidKasbonAmount(); + } +} \ No newline at end of file diff --git a/samooapk/app/Models/TimTeknisiPenugasan.php b/samooapk/app/Models/TimTeknisiPenugasan.php new file mode 100644 index 0000000..9fc17aa --- /dev/null +++ b/samooapk/app/Models/TimTeknisiPenugasan.php @@ -0,0 +1,72 @@ + 'datetime', + 'updated_at' => 'datetime', + ]; + + // =================================== + // RELATIONSHIPS + // =================================== + + /** + * Relasi ke penugasan + */ + public function penugasan() + { + return $this->belongsTo(Penugasan::class, 'id_penugasan', 'id_penugasan'); + } + + /** + * Relasi ke teknisi + */ + public function teknisi() + { + return $this->belongsTo(Teknisi::class, 'id_teknisi', 'id_teknisi'); + } + + // =================================== + // HELPER METHODS + // =================================== + + /** + * Cek apakah teknisi hadir + */ + public function isHadir(): bool + { + return $this->status_kehadiran === 'hadir'; + } + + /** + * Cek apakah teknisi tidak hadir + */ + public function isTidakHadir(): bool + { + return $this->status_kehadiran === 'tidak_hadir'; + } + + /** + * Cek apakah teknisi izin + */ + public function isIzin(): bool + { + return $this->status_kehadiran === 'izin'; + } +} \ No newline at end of file diff --git a/samooapk/app/Models/User.php b/samooapk/app/Models/User.php new file mode 100644 index 0000000..1dc791c --- /dev/null +++ b/samooapk/app/Models/User.php @@ -0,0 +1,45 @@ + + */ + protected $fillable = [ + 'name', + 'email', + 'password', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var array + */ + protected $hidden = [ + 'password', + 'remember_token', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + ]; +} diff --git a/samooapk/app/Providers/AppServiceProvider.php b/samooapk/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..55d88cd --- /dev/null +++ b/samooapk/app/Providers/AppServiceProvider.php @@ -0,0 +1,26 @@ + + */ + protected $policies = [ + // + ]; + + /** + * Register any authentication / authorization services. + */ + public function boot(): void + { + // + } +} diff --git a/samooapk/app/Providers/BroadcastServiceProvider.php b/samooapk/app/Providers/BroadcastServiceProvider.php new file mode 100644 index 0000000..2be04f5 --- /dev/null +++ b/samooapk/app/Providers/BroadcastServiceProvider.php @@ -0,0 +1,19 @@ +> + */ + protected $listen = [ + Registered::class => [ + SendEmailVerificationNotification::class, + ], + ]; + + /** + * Register any events for your application. + */ + public function boot(): void + { + // + } + + /** + * Determine if events and listeners should be automatically discovered. + */ + public function shouldDiscoverEvents(): bool + { + return false; + } +} diff --git a/samooapk/app/Providers/RouteServiceProvider.php b/samooapk/app/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..025e874 --- /dev/null +++ b/samooapk/app/Providers/RouteServiceProvider.php @@ -0,0 +1,40 @@ +by($request->user()?->id ?: $request->ip()); + }); + + $this->routes(function () { + Route::middleware('api') + ->prefix('api') + ->group(base_path('routes/api.php')); + + Route::middleware('web') + ->group(base_path('routes/web.php')); + }); + } +} diff --git a/samooapk/app/View/Components/AppLayout.php b/samooapk/app/View/Components/AppLayout.php new file mode 100644 index 0000000..de0d46f --- /dev/null +++ b/samooapk/app/View/Components/AppLayout.php @@ -0,0 +1,17 @@ +make(Illuminate\Contracts\Console\Kernel::class); + +$status = $kernel->handle( + $input = new Symfony\Component\Console\Input\ArgvInput, + new Symfony\Component\Console\Output\ConsoleOutput +); + +/* +|-------------------------------------------------------------------------- +| Shutdown The Application +|-------------------------------------------------------------------------- +| +| Once Artisan has finished running, we will fire off the shutdown events +| so that any final work may be done by the application before we shut +| down the process. This is the last thing to happen to the request. +| +*/ + +$kernel->terminate($input, $status); + +exit($status); diff --git a/samooapk/bootstrap/app.php b/samooapk/bootstrap/app.php new file mode 100644 index 0000000..037e17d --- /dev/null +++ b/samooapk/bootstrap/app.php @@ -0,0 +1,55 @@ +singleton( + Illuminate\Contracts\Http\Kernel::class, + App\Http\Kernel::class +); + +$app->singleton( + Illuminate\Contracts\Console\Kernel::class, + App\Console\Kernel::class +); + +$app->singleton( + Illuminate\Contracts\Debug\ExceptionHandler::class, + App\Exceptions\Handler::class +); + +/* +|-------------------------------------------------------------------------- +| Return The Application +|-------------------------------------------------------------------------- +| +| This script returns the application instance. The instance is given to +| the calling script so we can separate the building of the instances +| from the actual running of the application and sending responses. +| +*/ + +return $app; diff --git a/samooapk/bootstrap/cache/.gitignore b/samooapk/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/samooapk/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/samooapk/composer.json b/samooapk/composer.json new file mode 100644 index 0000000..328d3bf --- /dev/null +++ b/samooapk/composer.json @@ -0,0 +1,69 @@ +{ + "name": "laravel/laravel", + "type": "project", + "description": "The skeleton application for the Laravel framework.", + "keywords": ["laravel", "framework"], + "license": "MIT", + "require": { + "php": "^8.1", + "guzzlehttp/guzzle": "^7.2", + "laravel/framework": "^10.10", + "laravel/sanctum": "^3.3", + "laravel/tinker": "^2.8", + "resend/resend-laravel": "^1.4", + "tymon/jwt-auth": "^2.2" + }, + "require-dev": { + "fakerphp/faker": "^1.9.1", + "laravel/breeze": "^1.29", + "laravel/pint": "^1.0", + "laravel/sail": "^1.18", + "mockery/mockery": "^1.4.4", + "nunomaduro/collision": "^7.0", + "phpunit/phpunit": "^10.1", + "spatie/laravel-ignition": "^2.0" + }, + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Factories\\": "database/factories/", + "Database\\Seeders\\": "database/seeders/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "scripts": { + "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" + ] + }, + "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 +} diff --git a/samooapk/composer.lock b/samooapk/composer.lock new file mode 100644 index 0000000..bf014f4 --- /dev/null +++ b/samooapk/composer.lock @@ -0,0 +1,8633 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "809407e8ef896e251abee6b0c36480fc", + "packages": [ + { + "name": "brick/math", + "version": "0.12.3", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba", + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "6.8.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.12.3" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2025-02-28T13:11:00+00:00" + }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", + "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "doctrine/dbal": "<3.7.0 || >=4.0.0" + }, + "require-dev": { + "doctrine/dbal": "^3.7.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/2.1.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2023-12-11T17:09:12+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.0.10", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^11.0", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^8.5 || ^9.5", + "vimeo/psalm": "^4.25 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.0.10" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2024-02-18T20:23:39+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "8c784d071debd117328803d86b2097615b457500" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/8c784d071debd117328803d86b2097615b457500", + "reference": "8c784d071debd117328803d86b2097615b457500", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0", + "webmozart/assert": "^1.0" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.0", + "phpunit/phpunit": "^7.0|^8.0|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2024-10-09T13:47:03+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "symfony/http-foundation": "^4.4|^5.4|^6|^7" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2023-10-12T05:21:21+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:45:45+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.9.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-03-27T13:37:11+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.2.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-03-27T13:27:01+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2025-03-27T12:30:47+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.4", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "30e286560c137526eccd4ce21b2de477ab0676d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2", + "reference": "30e286560c137526eccd4ce21b2de477ab0676d2", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2025-02-03T10:55:03+00:00" + }, + { + "name": "laravel/framework", + "version": "v10.48.29", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "8f7f9247cb8aad1a769d6b9815a6623d89b46b47" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/8f7f9247cb8aad1a769d6b9815a6623d89b46b47", + "reference": "8f7f9247cb8aad1a769d6b9815a6623d89b46b47", + "shasum": "" + }, + "require": { + "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.3.2", + "egulias/email-validator": "^3.2.1|^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.2", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.1.9", + "laravel/serializable-closure": "^1.3", + "league/commonmark": "^2.2.1", + "league/flysystem": "^3.8.0", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^2.67", + "nunomaduro/termwind": "^1.13", + "php": "^8.1", + "psr/container": "^1.1.1|^2.0.1", + "psr/log": "^1.0|^2.0|^3.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^6.2", + "symfony/error-handler": "^6.2", + "symfony/finder": "^6.2", + "symfony/http-foundation": "^6.4", + "symfony/http-kernel": "^6.2", + "symfony/mailer": "^6.2", + "symfony/mime": "^6.2", + "symfony/process": "^6.2", + "symfony/routing": "^6.2", + "symfony/uid": "^6.2", + "symfony/var-dumper": "^6.2", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.4.1", + "voku/portable-ascii": "^2.0" + }, + "conflict": { + "carbonphp/carbon-doctrine-types": ">=3.0", + "doctrine/dbal": ">=4.0", + "mockery/mockery": "1.6.8", + "phpunit/phpunit": ">=11.0.0", + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.235.5", + "doctrine/dbal": "^3.5.1", + "ext-gmp": "*", + "fakerphp/faker": "^1.21", + "guzzlehttp/guzzle": "^7.5", + "league/flysystem-aws-s3-v3": "^3.0", + "league/flysystem-ftp": "^3.0", + "league/flysystem-path-prefixing": "^3.3", + "league/flysystem-read-only": "^3.3", + "league/flysystem-sftp-v3": "^3.0", + "mockery/mockery": "^1.5.1", + "nyholm/psr7": "^1.2", + "orchestra/testbench-core": "^8.23.4", + "pda/pheanstalk": "^4.0", + "phpstan/phpstan": "~1.11.11", + "phpunit/phpunit": "^10.0.7", + "predis/predis": "^2.0.2", + "symfony/cache": "^6.2", + "symfony/http-client": "^6.2.4", + "symfony/psr-http-message-bridge": "^2.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.235.5).", + "brianium/paratest": "Required to run tests in parallel (^6.0).", + "doctrine/dbal": "Required to rename columns and drop SQLite columns (^3.5.1).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0).", + "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "guzzlehttp/guzzle": "Required to use the HTTP Client and the ping methods on schedules (^7.5).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.0).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.0).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.3).", + "league/flysystem-read-only": "Required to use read-only disks (^3.3)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.0).", + "mockery/mockery": "Required to use mocking (^1.5.1).", + "nyholm/psr7": "Required to use PSR-7 bridging features (^1.2).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).", + "phpunit/phpunit": "Required to use assertions and run tests (^9.5.8|^10.0.7).", + "predis/predis": "Required to use the predis connector (^2.0.2).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^6.2).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^6.2).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^6.2).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^6.2).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^6.2).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^2.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-12T14:42:01+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.1.25", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "7b4029a84c37cb2725fc7f011586e2997040bc95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/7b4029a84c37cb2725fc7f011586e2997040bc95", + "reference": "7b4029a84c37cb2725fc7f011586e2997040bc95", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/collections": "^10.0|^11.0", + "php": "^8.1", + "symfony/console": "^6.2|^7.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3", + "phpstan/phpstan": "^1.11", + "phpstan/phpstan-mockery": "^1.1" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.1.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.1.25" + }, + "time": "2024-08-12T22:06:33+00:00" + }, + { + "name": "laravel/sanctum", + "version": "v3.3.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "8c104366459739f3ada0e994bcd3e6fd681ce3d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/8c104366459739f3ada0e994bcd3e6fd681ce3d5", + "reference": "8c104366459739f3ada0e994bcd3e6fd681ce3d5", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^9.21|^10.0", + "illuminate/contracts": "^9.21|^10.0", + "illuminate/database": "^9.21|^10.0", + "illuminate/support": "^9.21|^10.0", + "php": "^8.0.2" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^7.28.2|^8.8.3", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.6" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2023-12-19T18:44:48+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v1.3.7", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "4f48ade902b94323ca3be7646db16209ec76be3d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/4f48ade902b94323ca3be7646db16209ec76be3d", + "reference": "4f48ade902b94323ca3be7646db16209ec76be3d", + "shasum": "" + }, + "require": { + "php": "^7.3|^8.0" + }, + "require-dev": { + "illuminate/support": "^8.0|^9.0|^10.0|^11.0", + "nesbot/carbon": "^2.61|^3.0", + "pestphp/pest": "^1.21.3", + "phpstan/phpstan": "^1.8.2", + "symfony/var-dumper": "^5.4.11|^6.2.0|^7.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2024-11-14T18:34:49+00:00" + }, + { + "name": "laravel/tinker", + "version": "v2.10.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/22177cc71807d38f2810c6204d8f7183d88a57d3", + "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3", + "shasum": "" + }, + "require": { + "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "php": "^7.2.5|^8.0", + "psy/psysh": "^0.11.1|^0.12.0", + "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v2.10.1" + }, + "time": "2025-01-27T14:24:01+00:00" + }, + { + "name": "lcobucci/clock", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/clock.git", + "reference": "fb533e093fd61321bfcbac08b131ce805fe183d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/fb533e093fd61321bfcbac08b131ce805fe183d3", + "reference": "fb533e093fd61321bfcbac08b131ce805fe183d3", + "shasum": "" + }, + "require": { + "php": "^8.0", + "stella-maris/clock": "^0.1.4" + }, + "require-dev": { + "infection/infection": "^0.26", + "lcobucci/coding-standard": "^8.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-deprecation-rules": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpstan/phpstan-strict-rules": "^0.12", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\Clock\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com" + } + ], + "description": "Yet another clock abstraction", + "support": { + "issues": "https://github.com/lcobucci/clock/issues", + "source": "https://github.com/lcobucci/clock/tree/2.2.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2022-04-19T19:34:17+00:00" + }, + { + "name": "lcobucci/jwt", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "55564265fddf810504110bd68ca311932324b0e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/55564265fddf810504110bd68ca311932324b0e9", + "reference": "55564265fddf810504110bd68ca311932324b0e9", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-openssl": "*", + "lcobucci/clock": "^2.0", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "infection/infection": "^0.20", + "lcobucci/coding-standard": "^6.0", + "mikey179/vfsstream": "^1.6", + "phpbench/phpbench": "^0.17", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-deprecation-rules": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpstan/phpstan-strict-rules": "^0.12", + "phpunit/php-invoker": "^3.1", + "phpunit/phpunit": "^9.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com", + "role": "Developer" + } + ], + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "keywords": [ + "JWS", + "jwt" + ], + "support": { + "issues": "https://github.com/lcobucci/jwt/issues", + "source": "https://github.com/lcobucci/jwt/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2021-09-28T19:18:28+00:00" + }, + { + "name": "league/commonmark", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "10732241927d3971d28e7ea7b5712721fa2296ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca", + "reference": "10732241927d3971d28e7ea7b5712721fa2296ca", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2025-07-20T12:47:49+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.30.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2203e3151755d874bb2943649dae1eb8533ac93e", + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.30.0" + }, + "time": "2025-06-25T13:29:59+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.30.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/6691915f77c7fb69adfb87dcd550052dc184ee10", + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.0" + }, + "time": "2025-05-21T10:34:19+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-09-21T08:32:55+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.9.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2025-03-24T10:02:05+00:00" + }, + { + "name": "nesbot/carbon", + "version": "2.73.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "9228ce90e1035ff2f0db84b40ec2e023ed802075" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/9228ce90e1035ff2f0db84b40ec2e023ed802075", + "reference": "9228ce90e1035ff2f0db84b40ec2e023ed802075", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "*", + "ext-json": "*", + "php": "^7.1.8 || ^8.0", + "psr/clock": "^1.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php80": "^1.16", + "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^2.0 || ^3.1.4 || ^4.0", + "doctrine/orm": "^2.7 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.0", + "kylekatarnls/multi-tester": "^2.0", + "ondrejmirtes/better-reflection": "<6", + "phpmd/phpmd": "^2.9", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12.99 || ^1.7.14", + "phpunit/php-file-iterator": "^2.0.5 || ^3.0.6", + "phpunit/phpunit": "^7.5.20 || ^8.5.26 || ^9.5.20", + "squizlabs/php_codesniffer": "^3.4" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbon.nesbot.com/docs", + "issues": "https://github.com/briannesbitt/Carbon/issues", + "source": "https://github.com/briannesbitt/Carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2025-01-08T20:10:23+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.2", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "da801d52f0354f70a638673c4a0f04e16529431d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", + "reference": "da801d52f0354f70a638673c4a0f04e16529431d", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.4" + }, + "require-dev": { + "nette/tester": "^2.5.2", + "phpstan/phpstan-nette": "^1.0", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.2" + }, + "time": "2024-10-06T23:10:23+00:00" + }, + { + "name": "nette/utils", + "version": "v4.0.7", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/e67c4061eb40b9c113b218214e42cb5a0dda28f2", + "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2", + "shasum": "" + }, + "require": { + "php": "8.0 - 8.4" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "^2.5", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.0.7" + }, + "time": "2025-06-03T04:55:08+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.6.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/221b0d0fdf1369c71047ad1d18bb5880017bbc56", + "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.0" + }, + "time": "2025-07-27T20:03:57+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v1.17.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "5369ef84d8142c1d87e4ec278711d4ece3cbf301" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/5369ef84d8142c1d87e4ec278711d4ece3cbf301", + "reference": "5369ef84d8142c1d87e4ec278711d4ece3cbf301", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.4.15" + }, + "require-dev": { + "illuminate/console": "^10.48.24", + "illuminate/support": "^10.48.24", + "laravel/pint": "^1.18.2", + "pestphp/pest": "^2.36.0", + "pestphp/pest-plugin-mock": "2.0.0", + "phpstan/phpstan": "^1.12.11", + "phpstan/phpstan-strict-rules": "^1.6.1", + "symfony/var-dumper": "^6.4.15", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Its like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v1.17.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2024-11-21T10:36:35+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.3", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54", + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:41:07+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.10", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/6e80abe6f2257121f1eb9a4c55bf29d921025b22", + "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2" + }, + "suggest": { + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-pdo-sqlite": "The doc command requires SQLite to work.", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.10" + }, + "time": "2025-08-04T12:39:37+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.0", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/4e0e23cc785f0724a0e838279a9eb03f28b092a0", + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.0" + }, + "time": "2025-06-25T14:20:11+00:00" + }, + { + "name": "resend/resend-laravel", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/resend/resend-laravel.git", + "reference": "6dd5f5ec607404068c5af067fd7f6ba4b659262b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/resend/resend-laravel/zipball/6dd5f5ec607404068c5af067fd7f6ba4b659262b", + "reference": "6dd5f5ec607404068c5af067fd7f6ba4b659262b", + "shasum": "" + }, + "require": { + "illuminate/http": "^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "php": "^8.1", + "resend/resend-php": "^1.0.0", + "symfony/mailer": "^6.2|^7.0|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.14", + "mockery/mockery": "^1.5", + "orchestra/testbench": "^8.17|^9.0|^10.8|^11.0", + "pestphp/pest": "^1.0|^2.0|^3.7|^4.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Resend\\Laravel\\ResendServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Resend\\Laravel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Resend and contributors", + "homepage": "https://github.com/resend/resend-laravel/contributors" + } + ], + "description": "Resend for Laravel", + "homepage": "https://resend.com/", + "keywords": [ + "api", + "client", + "laravel", + "php", + "resend", + "sdk" + ], + "support": { + "issues": "https://github.com/resend/resend-laravel/issues", + "source": "https://github.com/resend/resend-laravel/tree/v1.4.0" + }, + "time": "2026-05-06T17:08:44+00:00" + }, + { + "name": "resend/resend-php", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/resend/resend-php.git", + "reference": "87d29d98271a0ab1c09cdbee102daa2f9b3419db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/resend/resend-php/zipball/87d29d98271a0ab1c09cdbee102daa2f9b3419db", + "reference": "87d29d98271a0ab1c09cdbee102daa2f9b3419db", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.5", + "php": "^8.1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.13", + "mockery/mockery": "^1.6", + "pestphp/pest": "^1.0|^2.0|^3.0|^4.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Resend.php" + ], + "psr-4": { + "Resend\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Resend and contributors", + "homepage": "https://github.com/resend/resend-php/contributors" + } + ], + "description": "Resend PHP library.", + "homepage": "https://resend.com/", + "keywords": [ + "api", + "client", + "php", + "resend", + "sdk" + ], + "support": { + "issues": "https://github.com/resend/resend-php/issues", + "source": "https://github.com/resend/resend-php/tree/v1.3.0" + }, + "time": "2026-04-11T10:48:32+00:00" + }, + { + "name": "stella-maris/clock", + "version": "0.1.7", + "source": { + "type": "git", + "url": "https://github.com/stella-maris-solutions/clock.git", + "reference": "fa23ce16019289a18bb3446fdecd45befcdd94f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stella-maris-solutions/clock/zipball/fa23ce16019289a18bb3446fdecd45befcdd94f8", + "reference": "fa23ce16019289a18bb3446fdecd45befcdd94f8", + "shasum": "" + }, + "require": { + "php": "^7.0|^8.0", + "psr/clock": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "StellaMaris\\Clock\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Heigl", + "role": "Maintainer" + } + ], + "description": "A pre-release of the proposed PSR-20 Clock-Interface", + "homepage": "https://gitlab.com/stella-maris/clock", + "keywords": [ + "clock", + "datetime", + "point in time", + "psr20" + ], + "support": { + "source": "https://github.com/stella-maris-solutions/clock/tree/0.1.7" + }, + "time": "2022-11-25T16:15:06+00:00" + }, + { + "name": "symfony/console", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "59266a5bf6a596e3e0844fd95e6ad7ea3c1d3350" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/59266a5bf6a596e3e0844fd95e6ad7ea3c1d3350", + "reference": "59266a5bf6a596e3e0844fd95e6ad7ea3c1d3350", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-30T10:38:54+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "30fd0b3cf0e972e82636038ce4db0e4fe777112c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/30fd0b3cf0e972e82636038ce4db0e4fe777112c", + "reference": "30fd0b3cf0e972e82636038ce4db0e4fe777112c", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/serializer": "^5.4|^6.0|^7.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-24T08:25:04+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "497f73ac996a598c92409b44ac43b6690c4f666d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/497f73ac996a598c92409b44ac43b6690c4f666d", + "reference": "497f73ac996a598c92409b44ac43b6690c4f666d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-22T09:11:45+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "73089124388c8510efb8d2d1689285d285937b08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/73089124388c8510efb8d2d1689285d285937b08", + "reference": "73089124388c8510efb8d2d1689285d285937b08", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T12:02:45+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "0341e41d8d8830c31a1dff5cbc5bdb3ec872a073" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/0341e41d8d8830c31a1dff5cbc5bdb3ec872a073", + "reference": "0341e41d8d8830c31a1dff5cbc5bdb3ec872a073", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:14:14+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "b81dcdbe34b8e8f7b3fc7b2a47fa065d5bf30726" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b81dcdbe34b8e8f7b3fc7b2a47fa065d5bf30726", + "reference": "b81dcdbe34b8e8f7b3fc7b2a47fa065d5bf30726", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<5.4", + "symfony/cache": "<5.4", + "symfony/config": "<6.1", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<5.4", + "symfony/form": "<5.4", + "symfony/http-client": "<5.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<5.4", + "symfony/messenger": "<5.4", + "symfony/translation": "<5.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<5.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.3", + "twig/twig": "<2.13" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^5.4|^6.0|^7.0", + "symfony/clock": "^6.2|^7.0", + "symfony/config": "^6.1|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/css-selector": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dom-crawler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4.5|^6.0.5|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4.4|^7.0.4", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4|^6.0|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.4|^7.0", + "symfony/var-exporter": "^6.2|^7.0", + "twig/twig": "^2.13|^3.0.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-31T09:23:30+00:00" + }, + { + "name": "symfony/mailer", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "b4d7fa2c69641109979ed06e98a588d245362062" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/b4d7fa2c69641109979ed06e98a588d245362062", + "reference": "b4d7fa2c69641109979ed06e98a588d245362062", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.1", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/mime": "^6.2|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4", + "symfony/messenger": "<6.2", + "symfony/mime": "<6.2", + "symfony/twig-bridge": "<6.2.1" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/messenger": "^6.2|^7.0", + "symfony/twig-bridge": "^6.2|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-24T08:25:04+00:00" + }, + { + "name": "symfony/mime", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "664d5e844a2de5e11c8255d0aef6bc15a9660ac7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/664d5e844a2de5e11c8255d0aef6bc15a9660ac7", + "reference": "664d5e844a2de5e11c8255d0aef6bc15a9660ac7", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<5.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.4|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T12:02:45+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/process", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "8eb6dc555bfb49b2703438d5de65cc9f138ff50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/8eb6dc555bfb49b2703438d5de65cc9f138ff50b", + "reference": "8eb6dc555bfb49b2703438d5de65cc9f138ff50b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:14:14+00:00" + }, + { + "name": "symfony/routing", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "e4f94e625c8e6f910aa004a0042f7b2d398278f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/e4f94e625c8e6f910aa004a0042f7b2d398278f5", + "reference": "e4f94e625c8e6f910aa004a0042f7b2d398278f5", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/annotations": "<1.12", + "symfony/config": "<6.2", + "symfony/dependency-injection": "<5.4", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.12|^2", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.2|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T08:46:37+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-25T09:37:31+00:00" + }, + { + "name": "symfony/string", + "version": "v7.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/42f505aff654e62ac7ac2ce21033818297ca89ca", + "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:47:49+00:00" + }, + { + "name": "symfony/translation", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "300b72643e89de0734d99a9e3f8494a3ef6936e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/300b72643e89de0734d99a9e3f8494a3ef6936e1", + "reference": "300b72643e89de0734d99a9e3f8494a3ef6936e1", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5|^3.0" + }, + "conflict": { + "symfony/config": "<5.4", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<5.4", + "symfony/yaml": "<5.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^4.18|^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-30T17:30:48+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-27T08:32:26+00:00" + }, + { + "name": "symfony/uid", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "17da16a750541a42cf2183935e0f6008316c23f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/17da16a750541a42cf2183935e0f6008316c23f7", + "reference": "17da16a750541a42cf2183935e0f6008316c23f7", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:14:14+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "aa29484ce0544bd69fa9f0df902e5ed7b7fe5034" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/aa29484ce0544bd69fa9f0df902e5ed7b7fe5034", + "reference": "aa29484ce0544bd69fa9f0df902e5ed7b7fe5034", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^6.3|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/uid": "^5.4|^6.0|^7.0", + "twig/twig": "^2.13|^3.0.4" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-29T18:40:01+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "0d72ac1c00084279c1816675284073c5a337c20d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", + "reference": "0d72ac1c00084279c1816675284073c5a337c20d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0" + }, + "time": "2024-12-21T16:25:41+00:00" + }, + { + "name": "tymon/jwt-auth", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/tymondesigns/jwt-auth.git", + "reference": "42381e56db1bf887c12e5302d11901d65cc74856" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tymondesigns/jwt-auth/zipball/42381e56db1bf887c12e5302d11901d65cc74856", + "reference": "42381e56db1bf887c12e5302d11901d65cc74856", + "shasum": "" + }, + "require": { + "illuminate/auth": "^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^9.0|^10.0|^11.0|^12.0", + "illuminate/http": "^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^9.0|^10.0|^11.0|^12.0", + "lcobucci/jwt": "^4.0", + "nesbot/carbon": "^2.69|^3.0", + "php": "^8.0" + }, + "require-dev": { + "illuminate/console": "^9.0|^10.0|^11.0|^12.0", + "illuminate/database": "^9.0|^10.0|^11.0|^12.0", + "illuminate/routing": "^9.0|^10.0|^11.0|^12.0", + "mockery/mockery": "^1.6", + "phpunit/phpunit": "^9.4" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "JWTAuth": "Tymon\\JWTAuth\\Facades\\JWTAuth", + "JWTFactory": "Tymon\\JWTAuth\\Facades\\JWTFactory" + }, + "providers": [ + "Tymon\\JWTAuth\\Providers\\LaravelServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.0-dev", + "dev-develop": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Tymon\\JWTAuth\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sean Tymon", + "email": "tymon148@gmail.com", + "homepage": "https://tymon.xyz", + "role": "Developer" + } + ], + "description": "JSON Web Token Authentication for Laravel and Lumen", + "homepage": "https://github.com/tymondesigns/jwt-auth", + "keywords": [ + "Authentication", + "JSON Web Token", + "auth", + "jwt", + "laravel" + ], + "support": { + "issues": "https://github.com/tymondesigns/jwt-auth/issues", + "source": "https://github.com/tymondesigns/jwt-auth" + }, + "funding": [ + { + "url": "https://www.patreon.com/seantymon", + "type": "patreon" + } + ], + "time": "2025-04-16T22:22:54+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.2", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.3", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-04-30T23:37:27+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2024-11-21T01:49:47+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" + } + ], + "packages-dev": [ + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.3", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "59a123a3d459c5a23055802237cb317f609867e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/59a123a3d459c5a23055802237cb317f609867e5", + "reference": "59a123a3d459c5a23055802237cb317f609867e5", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.3" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-06-16T00:02:10+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "laravel/breeze", + "version": "v1.29.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/breeze.git", + "reference": "22c53b84b7fff91b01a318d71a10dfc251e92849" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/breeze/zipball/22c53b84b7fff91b01a318d71a10dfc251e92849", + "reference": "22c53b84b7fff91b01a318d71a10dfc251e92849", + "shasum": "" + }, + "require": { + "illuminate/console": "^10.17", + "illuminate/filesystem": "^10.17", + "illuminate/support": "^10.17", + "illuminate/validation": "^10.17", + "php": "^8.1.0" + }, + "require-dev": { + "orchestra/testbench": "^8.0", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Breeze\\BreezeServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Breeze\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Minimal Laravel authentication scaffolding with Blade and Tailwind.", + "keywords": [ + "auth", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/breeze/issues", + "source": "https://github.com/laravel/breeze" + }, + "time": "2024-03-04T14:35:21+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.24.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/0345f3b05f136801af8c339f9d16ef29e6b4df8a", + "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.82.2", + "illuminate/view": "^11.45.1", + "larastan/larastan": "^3.5.0", + "laravel-zero/framework": "^11.45.0", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.3.1", + "pestphp/pest": "^2.36.0" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "files": [ + "overrides/Runner/Parallel/ProcessFactory.php" + ], + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2025-07-10T18:09:32+00:00" + }, + { + "name": "laravel/sail", + "version": "v1.44.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sail.git", + "reference": "a09097bd2a8a38e23ac472fa6a6cf5b0d1c1d3fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sail/zipball/a09097bd2a8a38e23ac472fa6a6cf5b0d1c1d3fe", + "reference": "a09097bd2a8a38e23ac472fa6a6cf5b0d1c1d3fe", + "shasum": "" + }, + "require": { + "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0", + "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0", + "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0", + "php": "^8.0", + "symfony/console": "^6.0|^7.0", + "symfony/yaml": "^6.0|^7.0" + }, + "require-dev": { + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", + "phpstan/phpstan": "^1.10" + }, + "bin": [ + "bin/sail" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sail\\SailServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Docker files for running a basic Laravel application.", + "keywords": [ + "docker", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/sail/issues", + "source": "https://github.com/laravel/sail" + }, + "time": "2025-07-04T16:17:06+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v7.12.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "995245421d3d7593a6960822063bdba4f5d7cf1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/995245421d3d7593a6960822063bdba4f5d7cf1a", + "reference": "995245421d3d7593a6960822063bdba4f5d7cf1a", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.17.0", + "nunomaduro/termwind": "^1.17.0", + "php": "^8.1.0", + "symfony/console": "^6.4.17" + }, + "conflict": { + "laravel/framework": ">=11.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.4.8", + "laravel/framework": "^10.48.29", + "laravel/pint": "^1.21.2", + "laravel/sail": "^1.41.0", + "laravel/sanctum": "^3.3.3", + "laravel/tinker": "^2.10.1", + "nunomaduro/larastan": "^2.10.0", + "orchestra/testbench-core": "^8.35.0", + "pestphp/pest": "^2.36.0", + "phpunit/phpunit": "^10.5.36", + "sebastian/environment": "^6.1.0", + "spatie/laravel-ignition": "^2.9.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2025-03-14T22:35:49+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:31:57+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.5.48", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "6e0a2bc39f6fae7617989d690d76c48e6d2eb541" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6e0a2bc39f6fae7617989d690d76c48e6d2eb541", + "reference": "6e0a2bc39f6fae7617989d690d76c48e6d2eb541", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.3", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.3", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.2", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.0", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.48" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-07-11T04:07:17+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:12:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-18T14:56:07+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68ff824baeae169ec9f2137158ee529584553799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:37:17+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-23T08:47:14+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "955288482d97c19a372d3f31006ab3f37da47adf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", + "reference": "955288482d97c19a372d3f31006ab3f37da47adf", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:17:12+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:19:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:38:20+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:05:40+00:00" + }, + { + "name": "sebastian/type", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" + }, + { + "name": "sebastian/version", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+00:00" + }, + { + "name": "spatie/backtrace", + "version": "1.7.4", + "source": { + "type": "git", + "url": "https://github.com/spatie/backtrace.git", + "reference": "cd37a49fce7137359ac30ecc44ef3e16404cccbe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/backtrace/zipball/cd37a49fce7137359ac30ecc44ef3e16404cccbe", + "reference": "cd37a49fce7137359ac30ecc44ef3e16404cccbe", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "ext-json": "*", + "laravel/serializable-closure": "^1.3 || ^2.0", + "phpunit/phpunit": "^9.3 || ^11.4.3", + "spatie/phpunit-snapshot-assertions": "^4.2 || ^5.1.6", + "symfony/var-dumper": "^5.1 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Backtrace\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van de Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "A better backtrace", + "homepage": "https://github.com/spatie/backtrace", + "keywords": [ + "Backtrace", + "spatie" + ], + "support": { + "source": "https://github.com/spatie/backtrace/tree/1.7.4" + }, + "funding": [ + { + "url": "https://github.com/sponsors/spatie", + "type": "github" + }, + { + "url": "https://spatie.be/open-source/support-us", + "type": "other" + } + ], + "time": "2025-05-08T15:41:09+00:00" + }, + { + "name": "spatie/error-solutions", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/spatie/error-solutions.git", + "reference": "e495d7178ca524f2dd0fe6a1d99a1e608e1c9936" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/error-solutions/zipball/e495d7178ca524f2dd0fe6a1d99a1e608e1c9936", + "reference": "e495d7178ca524f2dd0fe6a1d99a1e608e1c9936", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "illuminate/broadcasting": "^10.0|^11.0|^12.0", + "illuminate/cache": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "livewire/livewire": "^2.11|^3.5.20", + "openai-php/client": "^0.10.1", + "orchestra/testbench": "8.22.3|^9.0|^10.0", + "pestphp/pest": "^2.20|^3.0", + "phpstan/phpstan": "^2.1", + "psr/simple-cache": "^3.0", + "psr/simple-cache-implementation": "^3.0", + "spatie/ray": "^1.28", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "vlucas/phpdotenv": "^5.5" + }, + "suggest": { + "openai-php/client": "Require get solutions from OpenAI", + "simple-cache-implementation": "To cache solutions from OpenAI" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Ignition\\": "legacy/ignition", + "Spatie\\ErrorSolutions\\": "src", + "Spatie\\LaravelIgnition\\": "legacy/laravel-ignition" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "role": "Developer" + } + ], + "description": "This is my package error-solutions", + "homepage": "https://github.com/spatie/error-solutions", + "keywords": [ + "error-solutions", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/error-solutions/issues", + "source": "https://github.com/spatie/error-solutions/tree/1.1.3" + }, + "funding": [ + { + "url": "https://github.com/Spatie", + "type": "github" + } + ], + "time": "2025-02-14T12:29:50+00:00" + }, + { + "name": "spatie/flare-client-php", + "version": "1.10.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/flare-client-php.git", + "reference": "bf1716eb98bd689451b071548ae9e70738dce62f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/flare-client-php/zipball/bf1716eb98bd689451b071548ae9e70738dce62f", + "reference": "bf1716eb98bd689451b071548ae9e70738dce62f", + "shasum": "" + }, + "require": { + "illuminate/pipeline": "^8.0|^9.0|^10.0|^11.0|^12.0", + "php": "^8.0", + "spatie/backtrace": "^1.6.1", + "symfony/http-foundation": "^5.2|^6.0|^7.0", + "symfony/mime": "^5.2|^6.0|^7.0", + "symfony/process": "^5.2|^6.0|^7.0", + "symfony/var-dumper": "^5.2|^6.0|^7.0" + }, + "require-dev": { + "dms/phpunit-arraysubset-asserts": "^0.5.0", + "pestphp/pest": "^1.20|^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "spatie/pest-plugin-snapshots": "^1.0|^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\FlareClient\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Send PHP errors to Flare", + "homepage": "https://github.com/spatie/flare-client-php", + "keywords": [ + "exception", + "flare", + "reporting", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/flare-client-php/issues", + "source": "https://github.com/spatie/flare-client-php/tree/1.10.1" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-02-14T13:42:06+00:00" + }, + { + "name": "spatie/ignition", + "version": "1.15.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/ignition.git", + "reference": "31f314153020aee5af3537e507fef892ffbf8c85" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/ignition/zipball/31f314153020aee5af3537e507fef892ffbf8c85", + "reference": "31f314153020aee5af3537e507fef892ffbf8c85", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "php": "^8.0", + "spatie/error-solutions": "^1.0", + "spatie/flare-client-php": "^1.7", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "require-dev": { + "illuminate/cache": "^9.52|^10.0|^11.0|^12.0", + "mockery/mockery": "^1.4", + "pestphp/pest": "^1.20|^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "psr/simple-cache-implementation": "*", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "vlucas/phpdotenv": "^5.5" + }, + "suggest": { + "openai-php/client": "Require get solutions from OpenAI", + "simple-cache-implementation": "To cache solutions from OpenAI" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Spatie\\Ignition\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Spatie", + "email": "info@spatie.be", + "role": "Developer" + } + ], + "description": "A beautiful error page for PHP applications.", + "homepage": "https://flareapp.io/ignition", + "keywords": [ + "error", + "flare", + "laravel", + "page" + ], + "support": { + "docs": "https://flareapp.io/docs/ignition-for-laravel/introduction", + "forum": "https://twitter.com/flareappio", + "issues": "https://github.com/spatie/ignition/issues", + "source": "https://github.com/spatie/ignition" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-02-21T14:31:39+00:00" + }, + { + "name": "spatie/laravel-ignition", + "version": "2.9.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-ignition.git", + "reference": "1baee07216d6748ebd3a65ba97381b051838707a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/1baee07216d6748ebd3a65ba97381b051838707a", + "reference": "1baee07216d6748ebd3a65ba97381b051838707a", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "illuminate/support": "^10.0|^11.0|^12.0", + "php": "^8.1", + "spatie/ignition": "^1.15", + "symfony/console": "^6.2.3|^7.0", + "symfony/var-dumper": "^6.2.3|^7.0" + }, + "require-dev": { + "livewire/livewire": "^2.11|^3.3.5", + "mockery/mockery": "^1.5.1", + "openai-php/client": "^0.8.1|^0.10", + "orchestra/testbench": "8.22.3|^9.0|^10.0", + "pestphp/pest": "^2.34|^3.7", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan-deprecation-rules": "^1.1.1|^2.0", + "phpstan/phpstan-phpunit": "^1.3.16|^2.0", + "vlucas/phpdotenv": "^5.5" + }, + "suggest": { + "openai-php/client": "Require get solutions from OpenAI", + "psr/simple-cache-implementation": "Needed to cache solutions from OpenAI" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Flare": "Spatie\\LaravelIgnition\\Facades\\Flare" + }, + "providers": [ + "Spatie\\LaravelIgnition\\IgnitionServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\LaravelIgnition\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Spatie", + "email": "info@spatie.be", + "role": "Developer" + } + ], + "description": "A beautiful error page for Laravel applications.", + "homepage": "https://flareapp.io/ignition", + "keywords": [ + "error", + "flare", + "laravel", + "page" + ], + "support": { + "docs": "https://flareapp.io/docs/ignition-for-laravel/introduction", + "forum": "https://twitter.com/flareappio", + "issues": "https://github.com/spatie/laravel-ignition/issues", + "source": "https://github.com/spatie/laravel-ignition" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-02-20T13:13:55+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "b8d7d868da9eb0919e99c8830431ea087d6aae30" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/b8d7d868da9eb0919e99c8830431ea087d6aae30", + "reference": "b8d7d868da9eb0919e99c8830431ea087d6aae30", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:47:49+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.1" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/samooapk/config/app.php b/samooapk/config/app.php new file mode 100644 index 0000000..c303137 --- /dev/null +++ b/samooapk/config/app.php @@ -0,0 +1,188 @@ + 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 + | your application so that it is used when running Artisan tasks. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + 'asset_url' => env('ASSET_URL'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. We have gone + | ahead and set this to a sensible default for you out of the box. + | + */ + + 'timezone' => 'Asia/Jakarta', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by the translation service provider. You are free to set this value + | to any of the locales which will be supported by the application. + | + */ + + 'locale' => 'en', + + /* + |-------------------------------------------------------------------------- + | Application Fallback Locale + |-------------------------------------------------------------------------- + | + | The fallback locale determines the locale to use when the current one + | is not available. You may change the value to correspond to any of + | the language folders that are provided through your application. + | + */ + + 'fallback_locale' => 'en', + + /* + |-------------------------------------------------------------------------- + | Faker Locale + |-------------------------------------------------------------------------- + | + | This locale will be used by the Faker PHP library when generating fake + | data for your database seeds. For example, this will be used to get + | localized telephone numbers, street address information and more. + | + */ + + 'faker_locale' => 'en_US', + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is used by the Illuminate encrypter service and should be set + | to a random, 32 character string, otherwise these encrypted strings + | will not be safe. Please do this before deploying an application! + | + */ + + 'key' => env('APP_KEY'), + + 'cipher' => 'AES-256-CBC', + + /* + |-------------------------------------------------------------------------- + | 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' => 'file', + // 'store' => 'redis', + ], + + /* + |-------------------------------------------------------------------------- + | Autoloaded Service Providers + |-------------------------------------------------------------------------- + | + | The service providers listed here will be automatically loaded on the + | request to your application. Feel free to add your own services to + | this array to grant expanded functionality to your applications. + | + */ + + 'providers' => ServiceProvider::defaultProviders()->merge([ + /* + * Package Service Providers... + */ + + /* + * Application Service Providers... + */ + App\Providers\AppServiceProvider::class, + App\Providers\AuthServiceProvider::class, + // App\Providers\BroadcastServiceProvider::class, + App\Providers\EventServiceProvider::class, + App\Providers\RouteServiceProvider::class, + ])->toArray(), + + /* + |-------------------------------------------------------------------------- + | Class Aliases + |-------------------------------------------------------------------------- + | + | This array of class aliases will be registered when this application + | is started. However, feel free to register as many as you wish as + | the aliases are "lazy" loaded so they don't hinder performance. + | + */ + + 'aliases' => Facade::defaultAliases()->merge([ + // 'Example' => App\Facades\Example::class, + ])->toArray(), + +]; diff --git a/samooapk/config/auth.php b/samooapk/config/auth.php new file mode 100644 index 0000000..8ecf88a --- /dev/null +++ b/samooapk/config/auth.php @@ -0,0 +1,128 @@ + [ + 'guard' => 'web', + 'passwords' => 'users', + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | here which uses session storage and the Eloquent user provider. + | + | All authentication drivers have a user provider. This defines how the + | users are actually retrieved out of your database or other storage + | mechanisms used by this application to persist your user's data. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + + // Guard untuk API dengan JWT (digunakan untuk Teknisi Mobile App) + 'api' => [ + 'driver' => 'jwt', + 'provider' => 'akun_teknisi', + 'hash' => false, + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication drivers have a user provider. This defines how the + | users are actually retrieved out of your database or other storage + | mechanisms used by this application to persist your user's data. + | + | If you have multiple user tables or models you may configure multiple + | sources which represent each model / table. These sources may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => App\Models\User::class, + ], + + // Provider untuk Akun Teknisi (Mobile App) + 'akun_teknisi' => [ + 'driver' => 'eloquent', + 'model' => App\Models\AkunTeknisi::class, + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | You may specify multiple password reset configurations if you have more + | than one user table or model in the application and you want to have + | separate password reset settings based on the specific user types. + | + | 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' => 'password_reset_tokens', + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the amount of seconds before a password confirmation + | times out and the user is prompted to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => 10800, + +]; \ No newline at end of file diff --git a/samooapk/config/broadcasting.php b/samooapk/config/broadcasting.php new file mode 100644 index 0000000..2410485 --- /dev/null +++ b/samooapk/config/broadcasting.php @@ -0,0 +1,71 @@ + env('BROADCAST_DRIVER', 'null'), + + /* + |-------------------------------------------------------------------------- + | Broadcast Connections + |-------------------------------------------------------------------------- + | + | Here you may define all of the broadcast connections that will be used + | to broadcast events to other systems or over websockets. Samples of + | each available type of connection are provided inside this array. + | + */ + + 'connections' => [ + + 'pusher' => [ + 'driver' => 'pusher', + 'key' => env('PUSHER_APP_KEY'), + 'secret' => env('PUSHER_APP_SECRET'), + 'app_id' => env('PUSHER_APP_ID'), + 'options' => [ + 'cluster' => env('PUSHER_APP_CLUSTER'), + 'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com', + 'port' => env('PUSHER_PORT', 443), + 'scheme' => env('PUSHER_SCHEME', 'https'), + 'encrypted' => true, + 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https', + ], + 'client_options' => [ + // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html + ], + ], + + 'ably' => [ + 'driver' => 'ably', + 'key' => env('ABLY_KEY'), + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'default', + ], + + 'log' => [ + 'driver' => 'log', + ], + + 'null' => [ + 'driver' => 'null', + ], + + ], + +]; diff --git a/samooapk/config/cache.php b/samooapk/config/cache.php new file mode 100644 index 0000000..d4171e2 --- /dev/null +++ b/samooapk/config/cache.php @@ -0,0 +1,111 @@ + env('CACHE_DRIVER', 'file'), + + /* + |-------------------------------------------------------------------------- + | 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: "apc", "array", "database", "file", + | "memcached", "redis", "dynamodb", "octane", "null" + | + */ + + 'stores' => [ + + 'apc' => [ + 'driver' => 'apc', + ], + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'table' => 'cache', + 'connection' => null, + 'lock_connection' => null, + ], + + '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' => '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', + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, or 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(env('APP_NAME', 'laravel'), '_').'_cache_'), + +]; diff --git a/samooapk/config/cors.php b/samooapk/config/cors.php new file mode 100644 index 0000000..f2d44aa --- /dev/null +++ b/samooapk/config/cors.php @@ -0,0 +1,34 @@ + ['api/*', 'sanctum/csrf-cookie', 'storage/*'], + + 'allowed_methods' => ['*'], + + 'allowed_origins' => ['*'], + + 'allowed_origins_patterns' => [], + + 'allowed_headers' => ['*'], + + 'exposed_headers' => [], + + 'max_age' => 0, + + 'supports_credentials' => false, + +]; diff --git a/samooapk/config/database.php b/samooapk/config/database.php new file mode 100644 index 0000000..137ad18 --- /dev/null +++ b/samooapk/config/database.php @@ -0,0 +1,151 @@ + env('DB_CONNECTION', 'mysql'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Here are each of the database connections setup for your application. + | Of course, examples of configuring each database platform that is + | supported by Laravel is shown below to make development simple. + | + | + | All database work in Laravel is done through the PHP PDO facilities + | so make sure you have the driver for your particular database of + | choice installed on your machine before you begin development. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DATABASE_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8', + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => 'prefer', + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + '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 in the database. + | + */ + + 'migrations' => 'migrations', + + /* + |-------------------------------------------------------------------------- + | 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 APC or Memcached. Laravel makes it easy to dig right in. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), + ], + + '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'), + ], + + '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'), + ], + + ], + +]; diff --git a/samooapk/config/filesystems.php b/samooapk/config/filesystems.php new file mode 100644 index 0000000..0779511 --- /dev/null +++ b/samooapk/config/filesystems.php @@ -0,0 +1,41 @@ + env('FILESYSTEM_DISK', 'local'), + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app'), + 'throw' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', + 'visibility' => 'public', + 'throw' => 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, + ], + + ], + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; \ No newline at end of file diff --git a/samooapk/config/hashing.php b/samooapk/config/hashing.php new file mode 100644 index 0000000..0e8a0bb --- /dev/null +++ b/samooapk/config/hashing.php @@ -0,0 +1,54 @@ + 'bcrypt', + + /* + |-------------------------------------------------------------------------- + | Bcrypt Options + |-------------------------------------------------------------------------- + | + | Here you may specify the configuration options that should be used when + | passwords are hashed using the Bcrypt algorithm. This will allow you + | to control the amount of time it takes to hash the given password. + | + */ + + 'bcrypt' => [ + 'rounds' => env('BCRYPT_ROUNDS', 12), + 'verify' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Argon Options + |-------------------------------------------------------------------------- + | + | Here you may specify the configuration options that should be used when + | passwords are hashed using the Argon algorithm. These will allow you + | to control the amount of time it takes to hash the given password. + | + */ + + 'argon' => [ + 'memory' => 65536, + 'threads' => 1, + 'time' => 4, + 'verify' => true, + ], + +]; diff --git a/samooapk/config/jwt.php b/samooapk/config/jwt.php new file mode 100644 index 0000000..08661d2 --- /dev/null +++ b/samooapk/config/jwt.php @@ -0,0 +1,301 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return [ + + /* + |-------------------------------------------------------------------------- + | JWT Authentication Secret + |-------------------------------------------------------------------------- + | + | Don't forget to set this in your .env file, as it will be used to sign + | your tokens. A helper command is provided for this: + | `php artisan jwt:secret` + | + | Note: This will be used for Symmetric algorithms only (HMAC), + | since RSA and ECDSA use a private/public key combo (See below). + | + */ + + 'secret' => env('JWT_SECRET', 'HdykYAkpGa4pOREb4vbddz16ujQkqMxBvWdDg4F0fHEwISnJp8asJiDfBWG8qT5V'), + + /* + |-------------------------------------------------------------------------- + | JWT Authentication Keys + |-------------------------------------------------------------------------- + | + | The algorithm you are using, will determine whether your tokens are + | signed with a random string (defined in `JWT_SECRET`) or using the + | following public & private keys. + | + | Symmetric Algorithms: + | HS256, HS384 & HS512 will use `JWT_SECRET`. + | + | Asymmetric Algorithms: + | RS256, RS384 & RS512 / ES256, ES384 & ES512 will use the keys below. + | + */ + + 'keys' => [ + + /* + |-------------------------------------------------------------------------- + | Public Key + |-------------------------------------------------------------------------- + | + | A path or resource to your public key. + | + | E.g. 'file://path/to/public/key' + | + */ + + 'public' => env('JWT_PUBLIC_KEY'), + + /* + |-------------------------------------------------------------------------- + | Private Key + |-------------------------------------------------------------------------- + | + | A path or resource to your private key. + | + | E.g. 'file://path/to/private/key' + | + */ + + 'private' => env('JWT_PRIVATE_KEY'), + + /* + |-------------------------------------------------------------------------- + | Passphrase + |-------------------------------------------------------------------------- + | + | The passphrase for your private key. Can be null if none set. + | + */ + + 'passphrase' => env('JWT_PASSPHRASE'), + + ], + + /* + |-------------------------------------------------------------------------- + | JWT time to live + |-------------------------------------------------------------------------- + | + | Specify the length of time (in minutes) that the token will be valid for. + | Defaults to 1 hour. + | + | You can also set this to null, to yield a never expiring token. + | Some people may want this behaviour for e.g. a mobile app. + | This is not particularly recommended, so make sure you have appropriate + | systems in place to revoke the token if necessary. + | Notice: If you set this to null you should remove 'exp' element from 'required_claims' list. + | + */ + + 'ttl' => env('JWT_TTL', 60), + + /* + |-------------------------------------------------------------------------- + | Refresh time to live + |-------------------------------------------------------------------------- + | + | Specify the length of time (in minutes) that the token can be refreshed + | within. I.E. The user can refresh their token within a 2 week window of + | the original token being created until they must re-authenticate. + | Defaults to 2 weeks. + | + | You can also set this to null, to yield an infinite refresh time. + | Some may want this instead of never expiring tokens for e.g. a mobile app. + | This is not particularly recommended, so make sure you have appropriate + | systems in place to revoke the token if necessary. + | + */ + + 'refresh_ttl' => env('JWT_REFRESH_TTL', 20160), + + /* + |-------------------------------------------------------------------------- + | JWT hashing algorithm + |-------------------------------------------------------------------------- + | + | Specify the hashing algorithm that will be used to sign the token. + | + */ + + 'algo' => env('JWT_ALGO', Tymon\JWTAuth\Providers\JWT\Provider::ALGO_HS256), + + /* + |-------------------------------------------------------------------------- + | Required Claims + |-------------------------------------------------------------------------- + | + | Specify the required claims that must exist in any token. + | A TokenInvalidException will be thrown if any of these claims are not + | present in the payload. + | + */ + + 'required_claims' => [ + 'iss', + 'iat', + 'exp', + 'nbf', + 'sub', + 'jti', + ], + + /* + |-------------------------------------------------------------------------- + | Persistent Claims + |-------------------------------------------------------------------------- + | + | Specify the claim keys to be persisted when refreshing a token. + | `sub` and `iat` will automatically be persisted, in + | addition to the these claims. + | + | Note: If a claim does not exist then it will be ignored. + | + */ + + 'persistent_claims' => [ + // 'foo', + // 'bar', + ], + + /* + |-------------------------------------------------------------------------- + | Lock Subject + |-------------------------------------------------------------------------- + | + | This will determine whether a `prv` claim is automatically added to + | the token. The purpose of this is to ensure that if you have multiple + | authentication models e.g. `App\User` & `App\OtherPerson`, then we + | should prevent one authentication request from impersonating another, + | if 2 tokens happen to have the same id across the 2 different models. + | + | Under specific circumstances, you may want to disable this behaviour + | e.g. if you only have one authentication model, then you would save + | a little on token size. + | + */ + + 'lock_subject' => true, + + /* + |-------------------------------------------------------------------------- + | Leeway + |-------------------------------------------------------------------------- + | + | This property gives the jwt timestamp claims some "leeway". + | Meaning that if you have any unavoidable slight clock skew on + | any of your servers then this will afford you some level of cushioning. + | + | This applies to the claims `iat`, `nbf` and `exp`. + | + | Specify in seconds - only if you know you need it. + | + */ + + 'leeway' => env('JWT_LEEWAY', 0), + + /* + |-------------------------------------------------------------------------- + | Blacklist Enabled + |-------------------------------------------------------------------------- + | + | In order to invalidate tokens, you must have the blacklist enabled. + | If you do not want or need this functionality, then set this to false. + | + */ + + 'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true), + + /* + | ------------------------------------------------------------------------- + | Blacklist Grace Period + | ------------------------------------------------------------------------- + | + | When multiple concurrent requests are made with the same JWT, + | it is possible that some of them fail, due to token regeneration + | on every request. + | + | Set grace period in seconds to prevent parallel request failure. + | + */ + + 'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 0), + + /* + |-------------------------------------------------------------------------- + | Cookies encryption + |-------------------------------------------------------------------------- + | + | By default Laravel encrypt cookies for security reason. + | If you decide to not decrypt cookies, you will have to configure Laravel + | to not encrypt your cookie token by adding its name into the $except + | array available in the middleware "EncryptCookies" provided by Laravel. + | see https://laravel.com/docs/master/responses#cookies-and-encryption + | for details. + | + | Set it to true if you want to decrypt cookies. + | + */ + + 'decrypt_cookies' => false, + + /* + |-------------------------------------------------------------------------- + | Providers + |-------------------------------------------------------------------------- + | + | Specify the various providers used throughout the package. + | + */ + + 'providers' => [ + + /* + |-------------------------------------------------------------------------- + | JWT Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to create and decode the tokens. + | + */ + + 'jwt' => Tymon\JWTAuth\Providers\JWT\Lcobucci::class, + + /* + |-------------------------------------------------------------------------- + | Authentication Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to authenticate users. + | + */ + + 'auth' => Tymon\JWTAuth\Providers\Auth\Illuminate::class, + + /* + |-------------------------------------------------------------------------- + | Storage Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to store tokens in the blacklist. + | + */ + + 'storage' => Tymon\JWTAuth\Providers\Storage\Illuminate::class, + + ], + +]; diff --git a/samooapk/config/logging.php b/samooapk/config/logging.php new file mode 100644 index 0000000..c44d276 --- /dev/null +++ b/samooapk/config/logging.php @@ -0,0 +1,131 @@ + 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' => false, + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Out of + | the box, Laravel uses the Monolog PHP logging library. This gives + | you a variety of powerful log handlers / formatters to utilize. + | + | Available Drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", + | "custom", "stack" + | + */ + + 'channels' => [ + 'stack' => [ + 'driver' => 'stack', + 'channels' => ['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' => 14, + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => 'Laravel Log', + '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, + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'with' => [ + 'stream' => 'php://stderr', + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + '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'), + ], + ], + +]; diff --git a/samooapk/config/mail.php b/samooapk/config/mail.php new file mode 100644 index 0000000..7eb2c11 --- /dev/null +++ b/samooapk/config/mail.php @@ -0,0 +1,141 @@ + env('MAIL_MAILER', 'smtp'), + + /* + |-------------------------------------------------------------------------- + | 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 to be used while + | sending an e-mail. You will specify which one you are using for your + | mailers below. You are free to add additional mailers as required. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "log", "array", "failover", "roundrobin" + | + */ + + 'mailers' => [ + 'smtp' => [ + 'transport' => 'smtp', + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), + 'port' => env('MAIL_PORT', 587), + 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN'), + 'stream' => [ + 'ssl' => [ + 'allow_self_signed' => true, + 'verify_peer' => false, + 'verify_peer_name' => false, + ], + ], + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => null, + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'mailgun' => [ + 'transport' => 'mailgun', + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + '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', + ], + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + ], + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all e-mails 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 e-mails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', 'Example'), + ], + + /* + |-------------------------------------------------------------------------- + | Markdown Mail Settings + |-------------------------------------------------------------------------- + | + | If you are using Markdown based email rendering, you may configure your + | theme and component paths here, allowing you to customize the design + | of the emails. Or, you may simply stick with the Laravel defaults! + | + */ + + 'markdown' => [ + 'theme' => 'default', + + 'paths' => [ + resource_path('views/vendor/mail'), + ], + ], + +]; diff --git a/samooapk/config/queue.php b/samooapk/config/queue.php new file mode 100644 index 0000000..01c6b05 --- /dev/null +++ b/samooapk/config/queue.php @@ -0,0 +1,109 @@ + env('QUEUE_CONNECTION', 'sync'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection information for each server that + | is used by your application. A default configuration has been added + | for each back-end shipped with Laravel. You are free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'table' => 'jobs', + 'queue' => 'default', + 'retry_after' => 90, + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => 'localhost', + 'queue' => 'default', + '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' => 'default', + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => 90, + 'block_for' => null, + 'after_commit' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | 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', 'mysql'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control which database and table are used to store the jobs that + | have failed. You may change them to any database / table you wish. + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'mysql'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/samooapk/config/sanctum.php b/samooapk/config/sanctum.php new file mode 100644 index 0000000..35d75b3 --- /dev/null +++ b/samooapk/config/sanctum.php @@ -0,0 +1,83 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort() + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of numerous + | security scanning initiatives maintained by open source platforms + | that notify developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, + 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, + 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, + ], + +]; diff --git a/samooapk/config/services.php b/samooapk/config/services.php new file mode 100644 index 0000000..289df3a --- /dev/null +++ b/samooapk/config/services.php @@ -0,0 +1,39 @@ + [ + 'domain' => env('MAILGUN_DOMAIN'), + 'secret' => env('MAILGUN_SECRET'), + 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), + 'scheme' => 'https', + ], + + 'postmark' => [ + 'token' => env('POSTMARK_TOKEN'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'recaptcha' => [ + 'site_key' => env('RECAPTCHA_SITE_KEY'), + 'secret_key' => env('RECAPTCHA_SECRET_KEY'), + ], + +]; diff --git a/samooapk/config/session.php b/samooapk/config/session.php new file mode 100644 index 0000000..e738cb3 --- /dev/null +++ b/samooapk/config/session.php @@ -0,0 +1,214 @@ + env('SESSION_DRIVER', 'file'), + + /* + |-------------------------------------------------------------------------- + | 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 immediately expire on the browser closing, set that option. + | + */ + + 'lifetime' => env('SESSION_LIFETIME', 120), + + 'expire_on_close' => false, + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it is stored. All encryption will be run + | automatically by Laravel and you can use the Session like normal. + | + */ + + 'encrypt' => false, + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When using the native session driver, we need a location where session + | files may be stored. A default has been set for you but a different + | location may be specified. This is only needed for file sessions. + | + */ + + '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 we + | should use to manage the sessions. Of course, a sensible default is + | provided for you; however, you are free to change this as needed. + | + */ + + 'table' => 'sessions', + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | While using one of the framework's cache driven session backends you may + | list a cache store that should be used for these sessions. This value + | must match with one of the application's configured cache "stores". + | + | Affects: "apc", "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 cookie used to identify a session + | instance by ID. The name specified here will get used every time a + | new session cookie is created by the framework for every driver. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug(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 are free to change this when necessary. + | + */ + + 'path' => '/', + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | Here you may change the domain of the cookie used to identify a session + | in your application. This will determine which domains the cookie is + | available to in your application. A sensible default has been set. + | + */ + + '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. You are free to modify this option if needed. + | + */ + + '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" since this is a secure default value. + | + | Supported: "lax", "strict", "none", null + | + */ + + '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' => false, + +]; diff --git a/samooapk/config/view.php b/samooapk/config/view.php new file mode 100644 index 0000000..22b8a18 --- /dev/null +++ b/samooapk/config/view.php @@ -0,0 +1,36 @@ + [ + resource_path('views'), + ], + + /* + |-------------------------------------------------------------------------- + | Compiled View Path + |-------------------------------------------------------------------------- + | + | This option determines where all the compiled Blade templates will be + | stored for your application. Typically, this is within the storage + | directory. However, as usual, you are free to change this value. + | + */ + + 'compiled' => env( + 'VIEW_COMPILED_PATH', + realpath(storage_path('framework/views')) + ), + +]; diff --git a/samooapk/database/.gitignore b/samooapk/database/.gitignore new file mode 100644 index 0000000..9b19b93 --- /dev/null +++ b/samooapk/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/samooapk/database/factories/UserFactory.php b/samooapk/database/factories/UserFactory.php new file mode 100644 index 0000000..584104c --- /dev/null +++ b/samooapk/database/factories/UserFactory.php @@ -0,0 +1,44 @@ + + */ +class UserFactory extends Factory +{ + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * Define the model's default state. + * + * @return array + */ + 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, + ]); + } +} diff --git a/samooapk/database/migrations/2014_10_12_000000_create_users_table.php b/samooapk/database/migrations/2014_10_12_000000_create_users_table.php new file mode 100644 index 0000000..444fafb --- /dev/null +++ b/samooapk/database/migrations/2014_10_12_000000_create_users_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + } +}; diff --git a/samooapk/database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php b/samooapk/database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php new file mode 100644 index 0000000..81a7229 --- /dev/null +++ b/samooapk/database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php @@ -0,0 +1,28 @@ +string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('password_reset_tokens'); + } +}; diff --git a/samooapk/database/migrations/2019_08_19_000000_create_failed_jobs_table.php b/samooapk/database/migrations/2019_08_19_000000_create_failed_jobs_table.php new file mode 100644 index 0000000..249da81 --- /dev/null +++ b/samooapk/database/migrations/2019_08_19_000000_create_failed_jobs_table.php @@ -0,0 +1,32 @@ +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('failed_jobs'); + } +}; diff --git a/samooapk/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php b/samooapk/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php new file mode 100644 index 0000000..e828ad8 --- /dev/null +++ b/samooapk/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->morphs('tokenable'); + $table->string('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/samooapk/database/migrations/2025_08_07_033606_create_teknisis_table.php b/samooapk/database/migrations/2025_08_07_033606_create_teknisis_table.php new file mode 100644 index 0000000..e9d51b4 --- /dev/null +++ b/samooapk/database/migrations/2025_08_07_033606_create_teknisis_table.php @@ -0,0 +1,34 @@ +id('id_teknisi'); + $table->string('nama', 100); + $table->date('tanggal_lahir'); + $table->text('alamat'); + $table->string('email', 100)->unique()->nullable(); + $table->string('no_telephone', 15); + $table->date('tanggal_masuk'); + $table->enum('status', ['aktif', 'tidak_aktif'])->default('aktif'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('teknisis'); + } +}; diff --git a/samooapk/database/migrations/2025_08_07_033645_create_penggajians_table.php b/samooapk/database/migrations/2025_08_07_033645_create_penggajians_table.php new file mode 100644 index 0000000..4640945 --- /dev/null +++ b/samooapk/database/migrations/2025_08_07_033645_create_penggajians_table.php @@ -0,0 +1,126 @@ +id('id_penggajian'); + + // =================================== + // IDENTITAS & PERIODE + // =================================== + $table->unsignedBigInteger('id_teknisi') + ->comment('ID teknisi yang digaji'); + + $table->integer('periode_bulan') + ->check('periode_bulan >= 1 AND periode_bulan <= 12') + ->comment('Bulan periode gaji (1-12)'); + + $table->year('periode_tahun') + ->comment('Tahun periode gaji'); + + $table->date('tanggal_penggajian') + ->comment('Tanggal gaji dihitung/dibuat'); + + // =================================== + // PENDAPATAN (HANYA ONGKOS PEKERJAAN) + // =================================== + $table->decimal('total_ongkos_pekerjaan', 12, 2) + ->default(0.00) + ->comment('Total ongkos dari penugasan yang selesai bulan ini (sudah dibagi tim)'); + + $table->integer('jumlah_penugasan_selesai') + ->default(0) + ->comment('Jumlah penugasan yang diselesaikan bulan ini'); + + $table->integer('jumlah_hari_kerja') + ->default(0) + ->comment('Jumlah hari kerja efektif bulan ini'); + + // =================================== + // POTONGAN (HANYA KASBON & BIAYA MAKAN) + // =================================== + $table->decimal('total_kasbon', 12, 2) + ->default(0.00) + ->comment('Total kasbon yang diambil bulan ini'); + + $table->decimal('biaya_makan', 12, 2) + ->default(0.00) + ->comment('Biaya makan yang dipotong dari gaji (hari kerja × tarif makan)'); + + $table->decimal('total_potongan', 12, 2) + ->default(0.00) + ->comment('Total potongan = kasbon + biaya_makan'); + + // =================================== + // GAJI BERSIH + // =================================== + $table->decimal('gaji_bersih', 12, 2) + ->default(0.00) + ->comment('Gaji bersih = ongkos_pekerjaan - total_potongan'); + + // =================================== + // STATUS & PEMBAYARAN + // =================================== + $table->enum('status_pembayaran', ['belum_bayar', 'sudah_bayar']) + ->default('belum_bayar') + ->comment('Status pembayaran gaji'); + + $table->enum('metode_pembayaran', ['cash', 'transfer']) + ->nullable() + ->comment('Metode pembayaran'); + + $table->dateTime('tanggal_dibayar') + ->nullable() + ->comment('Tanggal gaji benar-benar dibayarkan'); + + $table->text('bukti_pembayaran') + ->nullable() + ->comment('Path file/foto bukti transfer'); + + // =================================== + // CATATAN + // =================================== + $table->text('catatan') + ->nullable() + ->comment('Catatan tambahan untuk slip gaji ini'); + + $table->timestamps(); + + // =================================== + // FOREIGN KEYS + // =================================== + $table->foreign('id_teknisi') + ->references('id_teknisi') + ->on('teknisis') + ->onDelete('cascade'); + + // =================================== + // INDEXES + // =================================== + $table->unique(['id_teknisi', 'periode_bulan', 'periode_tahun'], 'unique_teknisi_periode'); + $table->index(['periode_tahun', 'periode_bulan'], 'idx_periode'); + $table->index('status_pembayaran', 'idx_status_bayar'); + $table->index('tanggal_penggajian', 'idx_tgl_gaji'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('penggajians'); + } +}; diff --git a/samooapk/database/migrations/2025_08_07_033736_create_tarif_pekerjaans_table.php b/samooapk/database/migrations/2025_08_07_033736_create_tarif_pekerjaans_table.php new file mode 100644 index 0000000..4441aee --- /dev/null +++ b/samooapk/database/migrations/2025_08_07_033736_create_tarif_pekerjaans_table.php @@ -0,0 +1,88 @@ +id('id_tarif'); + + // =================================== + // KATEGORI & JENIS PEKERJAAN + // =================================== + $table->enum('jenis_pekerjaan', [ + 'sr', // 1. Sambungan Rumah (meteran air) + 'pengembangan_jaringan_pipa', // 2. Pengembangan Jaringan Pipa (pipa baru) + 'pengangkatan', // 3. Pengangkatan meteran/gate valve + 'pemasangan_gate_valve', // 4. Pemasangan Gate Valve + Pressure + 'gali_urug', // 5. Gali Urug + 'perbaikan_jaringan_pipa', // 6. Perbaikan Jaringan Pipa Bocor + 'pengecatan_pipa_besi', // 7. Pengecatan Pipa Besi + 'penyempurnaan_jaringan_pipa' // 8. Penyempurnaan Jaringan Pipa + ])->comment('Jenis pekerjaan'); + + // =================================== + // IDENTITAS TARIF + // =================================== + $table->string('nama_item', 100)->comment('Nama item/pekerjaan'); + $table->string('kode_item', 20)->unique()->comment('Kode unik item'); + + // =================================== + // DIMENSI PIPA (untuk pekerjaan yang bergantung diameter) + // =================================== + $table->enum('dimensi_pipa', ['1-2', '3', '4', '6', '8', '10', '12']) + ->nullable() + ->comment('Dimensi/diameter pipa dalam inchi. NULL untuk pekerjaan yang tidak butuh dimensi pipa'); + + // =================================== + // TARIF PEKERJAAN + // =================================== + $table->decimal('tarif_per_unit', 12, 2) + ->nullable() + ->comment('Tarif flat per unit/titik. Dipakai untuk: SR (150k), Pengangkatan (200k), Gate Valve (200k/free), Gali Urug (200k/titik), Perbaikan & Pengecatan per dimensi'); + + $table->decimal('tarif_per_meter', 12, 2) + ->nullable() + ->comment('Tarif per meter. Dipakai untuk: Pengembangan & Penyempurnaan Jaringan Pipa sesuai dimensi'); + + // =================================== + // KONDISI KHUSUS + // =================================== + $table->boolean('pakai_pipa_besi') + ->nullable() + ->comment('Khusus Pemasangan Gate Valve: TRUE=ada pipa besi(200k), FALSE=tanpa pipa besi(free/0)'); + + // =================================== + // GARANSI (Khusus SR - Pemasangan Meteran Air) + // =================================== + $table->boolean('ada_garansi') + ->default(false) + ->comment('Apakah pekerjaan ini memiliki garansi?'); + + $table->integer('durasi_garansi_bulan') + ->nullable() + ->comment('Durasi garansi dalam bulan (3 bulan untuk SR)'); + + // =================================== + // INFO TAMBAHAN + // =================================== + $table->text('deskripsi')->nullable()->comment('Keterangan tambahan'); + + $table->boolean('is_active') + ->default(true) + ->comment('Status aktif tarif'); + + $table->timestamps(); + $table->softDeletes(); + }); + } + + public function down(): void + { + Schema::dropIfExists('tarif_pekerjaans'); + } +}; \ No newline at end of file diff --git a/samooapk/database/migrations/2025_08_07_033759_create_absensis_table.php b/samooapk/database/migrations/2025_08_07_033759_create_absensis_table.php new file mode 100644 index 0000000..11bf01a --- /dev/null +++ b/samooapk/database/migrations/2025_08_07_033759_create_absensis_table.php @@ -0,0 +1,39 @@ +id('id_absensi'); + $table->unsignedBigInteger('id_teknisi'); + $table->date('tanggal'); + $table->time('jam_masuk')->nullable(); + $table->time('jam_keluar')->nullable(); + $table->string('foto_absen_masuk', 255)->nullable(); + $table->string('foto_absen_keluar', 255)->nullable(); + $table->enum('status', ['hadir', 'izin', 'sakit', 'alpha']); + $table->text('keterangan')->nullable(); + $table->timestamps(); + + $table->foreign('id_teknisi')->references('id_teknisi')->on('teknisis')->onDelete('cascade'); + $table->unique(['id_teknisi', 'tanggal'], 'unique_teknisi_tanggal'); + $table->index('tanggal'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('absensis'); + } +}; diff --git a/samooapk/database/migrations/2025_08_07_033800_create_penugasans_table.php b/samooapk/database/migrations/2025_08_07_033800_create_penugasans_table.php new file mode 100644 index 0000000..87dcc3f --- /dev/null +++ b/samooapk/database/migrations/2025_08_07_033800_create_penugasans_table.php @@ -0,0 +1,126 @@ +id('id_penugasan'); + + // =================================== + // INFO PENUGASAN DASAR (Input Mandor) + // =================================== + $table->unsignedBigInteger('id_teknisi')->comment('Teknisi utama yang ditugaskan'); + $table->text('foto_surat')->nullable()->comment('Path foto surat tugas dari kantor'); // ✅ BARU + $table->date('tanggal_diberikan')->comment('Tanggal tugas diberikan'); + $table->text('catatan_admin')->nullable()->comment('Catatan/instruksi dari mandor'); + + // =================================== + // JENIS PEKERJAAN (Diisi Teknisi via Mobile) + // =================================== + $table->enum('jenis_pekerjaan', [ + 'sr', + 'pengembangan_jaringan_pipa', + 'pengangkatan', + 'pemasangan_gate_valve', + 'gali_urug', + 'perbaikan_jaringan_pipa', + 'pengecatan_pipa_besi', + 'penyempurnaan_jaringan_pipa' + ])->nullable()->comment('Jenis pekerjaan yang ditugaskan'); + + // =================================== + // DETAIL PEKERJAAN (Diisi Teknisi via Mobile) - SEMUA NULLABLE + // =================================== + $table->enum('dimensi_pipa', ['1-2', '3', '4', '6', '8', '10', '12']) + ->nullable() + ->comment('Dimensi pipa dalam inchi'); + + $table->decimal('jarak_meter', 10, 2) + ->nullable() + ->comment('Panjang pipa dalam meter'); + + $table->integer('jumlah_unit') + ->nullable() + ->comment('Jumlah unit. Untuk: SR, pengangkatan'); + + $table->integer('jumlah_titik') + ->nullable() + ->comment('Jumlah titik galian. Untuk: gali urug'); + + $table->boolean('pakai_pipa_besi') + ->nullable() + ->comment('Khusus pemasangan gate valve'); + + $table->enum('jenis_pengangkatan', ['meteran', 'gate_valve']) + ->nullable() + ->comment('Khusus pengangkatan: meteran air atau gate valve terpendam'); + + $table->text('detail_pekerjaan') + ->nullable() + ->comment('Catatan detail dari teknisi'); + + // =================================== + // ONGKOS PEKERJAAN + // =================================== + $table->unsignedBigInteger('id_tarif') + ->nullable() + ->comment('Referensi ke tabel tarif_pekerjaans'); + + $table->decimal('total_nilai_pekerjaan', 15, 2) + ->nullable() + ->comment('Total ongkos pekerjaan kotor (belum dibagi tim)'); + + // =================================== + // STATUS & TRACKING + // =================================== + $table->enum('status_pekerjaan', [ + 'belum_mulai', + 'dalam_proses', + 'selesai', + 'dibatalkan' + ])->default('belum_mulai'); + + $table->dateTime('tanggal_mulai')->nullable()->comment('Kapan teknisi mulai kerja'); + $table->dateTime('tanggal_diselesaikan')->nullable()->comment('Kapan pekerjaan selesai'); + + // =================================== + // GARANSI (Khusus SR - 3 bulan) + // =================================== + $table->date('tanggal_garansi_mulai')->nullable(); + $table->date('tanggal_garansi_selesai')->nullable(); + $table->text('catatan_garansi')->nullable(); + + // =================================== + // FOTO BUKTI PEKERJAAN (dari teknisi via mobile) + // =================================== + $table->text('foto_sebelum')->nullable()->comment('Path foto sebelum pekerjaan'); + $table->text('foto_sesudah')->nullable()->comment('Path foto setelah pekerjaan'); + + $table->timestamps(); + $table->softDeletes(); + + // =================================== + // FOREIGN KEYS + // =================================== + $table->foreign('id_teknisi') + ->references('id_teknisi') + ->on('teknisis') + ->onDelete('cascade'); + + $table->foreign('id_tarif') + ->references('id_tarif') + ->on('tarif_pekerjaans') + ->onDelete('set null'); + }); + } + + public function down(): void + { + Schema::dropIfExists('penugasans'); + } +}; \ No newline at end of file diff --git a/samooapk/database/migrations/2025_08_07_033844_create_kasbons_table.php b/samooapk/database/migrations/2025_08_07_033844_create_kasbons_table.php new file mode 100644 index 0000000..23ceb07 --- /dev/null +++ b/samooapk/database/migrations/2025_08_07_033844_create_kasbons_table.php @@ -0,0 +1,127 @@ +id('id_kasbon'); + + // =================================== + // IDENTITAS + // =================================== + $table->unsignedBigInteger('id_teknisi') + ->comment('ID teknisi yang ngasbon'); + + // =================================== + // DETAIL KASBON + // =================================== + $table->decimal('jumlah_kasbon', 12, 2) + ->comment('Jumlah uang yang dikasbon'); + + $table->date('tanggal_kasbon') + ->comment('Tanggal kasbon diambil'); + + $table->integer('periode_bulan') + ->comment('Bulan periode kasbon (1-12)') + ->check('periode_bulan >= 1 AND periode_bulan <= 12'); + + $table->year('periode_tahun') + ->comment('Tahun periode kasbon'); + + // =================================== + // KETERANGAN (OPSIONAL) + // =================================== + $table->string('keperluan', 100) + ->nullable() + ->comment('Keperluan kasbon (dropdown: biaya sekolah, obat, dll). OPSIONAL'); + + $table->text('keterangan_detail') + ->nullable() + ->comment('Keterangan tambahan jika perlu. OPSIONAL'); + + // =================================== + // PEMBERIAN KASBON (Oleh Mandor) + // =================================== + $table->enum('metode_pemberian', ['cash', 'transfer']) + ->default('cash') + ->comment('Metode pemberian kasbon oleh mandor'); + + // =================================== + // BUKTI KASBON (OPSIONAL) + // =================================== + $table->text('bukti_kasbon') + ->nullable() + ->comment('Path foto buku kasbon. OPSIONAL - bisa NULL untuk fase awal'); + + // =================================== + // TRACKING (Untuk Audit) + // =================================== + $table->unsignedBigInteger('diinput_oleh') + ->nullable() + ->comment('ID user/mandor yang input kasbon ini'); + + $table->dateTime('waktu_input') + ->default(DB::raw('CURRENT_TIMESTAMP')) + ->comment('Waktu kasbon diinput ke sistem (untuk audit)'); + + // =================================== + // STATUS & PELUNASAN + // =================================== + $table->enum('status', ['belum_lunas', 'lunas']) + ->default('belum_lunas') + ->comment('Status pelunasan kasbon'); + + $table->dateTime('tanggal_dilunasi') + ->nullable() + ->comment('Tanggal kasbon dilunasi (dipotong dari gaji)'); + + $table->unsignedBigInteger('id_penggajian') + ->nullable() + ->comment('ID penggajian yang memotong kasbon ini'); + + $table->text('catatan_pelunasan') + ->nullable() + ->comment('Catatan saat pelunasan'); + + $table->timestamps(); + + // =================================== + // FOREIGN KEYS + // =================================== + $table->foreign('id_teknisi') + ->references('id_teknisi') + ->on('teknisis') + ->onDelete('cascade'); + + $table->foreign('id_penggajian') + ->references('id_penggajian') + ->on('penggajians') + ->onDelete('set null'); + + // ✅ TAMBAHAN: Foreign key untuk diinput_oleh + $table->foreign('diinput_oleh') + ->references('id') + ->on('users') + ->onDelete('set null'); + + // =================================== + // INDEXES + // =================================== + $table->index('tanggal_kasbon', 'idx_tgl_kasbon'); + $table->index('status', 'idx_status'); + $table->index(['periode_tahun', 'periode_bulan'], 'idx_periode'); + $table->index('id_penggajian', 'idx_penggajian'); + }); + } + + public function down(): void + { + Schema::dropIfExists('kasbons'); + } +}; \ No newline at end of file diff --git a/samooapk/database/migrations/2025_08_07_033859_create_detail_penggajians_table.php b/samooapk/database/migrations/2025_08_07_033859_create_detail_penggajians_table.php new file mode 100644 index 0000000..fd70d25 --- /dev/null +++ b/samooapk/database/migrations/2025_08_07_033859_create_detail_penggajians_table.php @@ -0,0 +1,65 @@ +id('id_detail'); + + $table->unsignedBigInteger('id_penggajian') + ->comment('ID penggajian'); + + $table->unsignedBigInteger('id_penugasan') + ->comment('ID penugasan yang jadi sumber ongkos'); + + $table->date('tanggal_selesai') + ->comment('Tanggal penugasan selesai'); + + $table->string('lokasi') + ->comment('Lokasi pekerjaan'); + + $table->decimal('ongkos_penugasan', 12, 2) + ->comment('Total ongkos dari penugasan ini (kotor)'); + + $table->integer('jumlah_tim') + ->comment('Jumlah anggota tim yang hadir'); + + $table->decimal('bagian_ongkos', 12, 2) + ->comment('Bagian ongkos untuk teknisi ini (ongkos ÷ jumlah tim)'); + + $table->timestamps(); + + // Foreign Keys + $table->foreign('id_penggajian') + ->references('id_penggajian') + ->on('penggajians') + ->onDelete('cascade'); + + $table->foreign('id_penugasan') + ->references('id_penugasan') + ->on('penugasans') + ->onDelete('cascade'); + + // Index + $table->index('id_penggajian'); + $table->index('id_penugasan'); + }); + } + + public function down(): void + { + Schema::dropIfExists('detail_penggajians'); + } +}; \ No newline at end of file diff --git a/samooapk/database/migrations/2025_08_11_023136_create_akun_teknisis_table.php b/samooapk/database/migrations/2025_08_11_023136_create_akun_teknisis_table.php new file mode 100644 index 0000000..9b993e2 --- /dev/null +++ b/samooapk/database/migrations/2025_08_11_023136_create_akun_teknisis_table.php @@ -0,0 +1,34 @@ +id('id_akun_teknisi'); // Primary Key + $table->unsignedBigInteger('id_teknisi')->unique(); // Foreign Key ke tabel teknisis + $table->string('username')->unique(); + $table->string('password'); + $table->enum('status', ['aktif', 'tidak_aktif'])->default('aktif'); + $table->timestamps(); + + // Definisi Foreign Key Constraint + $table->foreign('id_teknisi')->references('id_teknisi')->on('teknisis')->onDelete('cascade'); + }); + } + + /** + * Kembalikan migrasi. + */ + public function down(): void + { + Schema::dropIfExists('akun_teknisis'); + } +}; \ No newline at end of file diff --git a/samooapk/database/migrations/2025_12_25_110336_create_tim_teknisi_penugasans_table.php b/samooapk/database/migrations/2025_12_25_110336_create_tim_teknisi_penugasans_table.php new file mode 100644 index 0000000..47ebe08 --- /dev/null +++ b/samooapk/database/migrations/2025_12_25_110336_create_tim_teknisi_penugasans_table.php @@ -0,0 +1,75 @@ +id('id_tim_penugasan'); + + // =================================== + // RELASI + // =================================== + $table->unsignedBigInteger('id_penugasan') + ->comment('ID penugasan yang dikerjakan'); + + $table->unsignedBigInteger('id_teknisi') + ->comment('ID teknisi anggota tim'); + + // =================================== + // STATUS KEHADIRAN + // Penting untuk perhitungan pembagian ongkos + // =================================== + $table->enum('status_kehadiran', ['hadir', 'tidak_hadir', 'izin']) + ->default('hadir') + ->comment('Status kehadiran teknisi. Hanya yang hadir dapat ongkos.'); + + // =================================== + // CATATAN + // =================================== + $table->text('catatan')->nullable() + ->comment('Catatan tambahan (misal: terlambat, pulang cepat, dll)'); + + $table->timestamps(); + + // =================================== + // FOREIGN KEYS + // =================================== + $table->foreign('id_penugasan') + ->references('id_penugasan') + ->on('penugasans') + ->onDelete('cascade'); + + $table->foreign('id_teknisi') + ->references('id_teknisi') + ->on('teknisis') + ->onDelete('cascade'); + + // =================================== + // UNIQUE CONSTRAINT + // Satu teknisi tidak bisa masuk 2x di penugasan yang sama + // =================================== + $table->unique(['id_penugasan', 'id_teknisi'], 'unique_penugasan_teknisi'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('tim_teknisi_penugasans'); + } +}; \ No newline at end of file diff --git a/samooapk/database/migrations/2026_04_24_125358_create_penugasan_items_table.php b/samooapk/database/migrations/2026_04_24_125358_create_penugasan_items_table.php new file mode 100644 index 0000000..97dd693 --- /dev/null +++ b/samooapk/database/migrations/2026_04_24_125358_create_penugasan_items_table.php @@ -0,0 +1,42 @@ +id('id_penugasan_item'); + $table->unsignedBigInteger('id_penugasan'); + $table->unsignedBigInteger('id_tarif')->nullable(); + + $table->string('jenis_pekerjaan'); + $table->string('dimensi_pipa')->nullable(); + $table->decimal('jarak_meter', 10, 2)->nullable(); + $table->integer('jumlah_unit')->nullable(); + $table->integer('jumlah_titik')->nullable(); + $table->boolean('pakai_pipa_besi')->nullable(); + $table->string('jenis_pengangkatan')->nullable(); // gate_valve atau meteran_air + + $table->decimal('total_nilai_pekerjaan', 15, 2)->default(0); + $table->timestamps(); + + $table->foreign('id_penugasan')->references('id_penugasan')->on('penugasans')->onDelete('cascade'); + $table->foreign('id_tarif')->references('id_tarif')->on('tarif_pekerjaans')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('penugasan_items'); + } +}; diff --git a/samooapk/database/migrations/2026_05_02_151800_add_structured_info_to_penugasans_table.php b/samooapk/database/migrations/2026_05_02_151800_add_structured_info_to_penugasans_table.php new file mode 100644 index 0000000..5eb52cc --- /dev/null +++ b/samooapk/database/migrations/2026_05_02_151800_add_structured_info_to_penugasans_table.php @@ -0,0 +1,24 @@ +string('alamat_lokasi')->nullable()->after('tanggal_diberikan'); + $table->string('nama_pelanggan')->nullable()->after('alamat_lokasi'); + $table->string('no_sambungan')->nullable()->after('nama_pelanggan'); + }); + } + + public function down(): void + { + Schema::table('penugasans', function (Blueprint $table) { + $table->dropColumn(['alamat_lokasi', 'nama_pelanggan', 'no_sambungan']); + }); + } +}; diff --git a/samooapk/database/migrations/2026_05_05_230156_add_rincian_pekerjaan_to_detail_penggajians_table.php b/samooapk/database/migrations/2026_05_05_230156_add_rincian_pekerjaan_to_detail_penggajians_table.php new file mode 100644 index 0000000..97af572 --- /dev/null +++ b/samooapk/database/migrations/2026_05_05_230156_add_rincian_pekerjaan_to_detail_penggajians_table.php @@ -0,0 +1,29 @@ +string('rincian_pekerjaan')->nullable()->after('bagian_ongkos') + ->comment('Rincian pekerjaan (misal: 10 Meter, 2 Unit)'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('detail_penggajians', function (Blueprint $table) { + $table->dropColumn('rincian_pekerjaan'); + }); + } +}; diff --git a/samooapk/database/migrations/2026_05_13_000001_add_password_plain_to_akun_teknisis.php b/samooapk/database/migrations/2026_05_13_000001_add_password_plain_to_akun_teknisis.php new file mode 100644 index 0000000..902c2da --- /dev/null +++ b/samooapk/database/migrations/2026_05_13_000001_add_password_plain_to_akun_teknisis.php @@ -0,0 +1,28 @@ +string('password_plain')->nullable()->after('password'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('akun_teknisis', function (Blueprint $table) { + $table->dropColumn('password_plain'); + }); + } +}; diff --git a/samooapk/database/migrations/2026_05_19_082612_add_location_to_absensis_table.php b/samooapk/database/migrations/2026_05_19_082612_add_location_to_absensis_table.php new file mode 100644 index 0000000..44cfe77 --- /dev/null +++ b/samooapk/database/migrations/2026_05_19_082612_add_location_to_absensis_table.php @@ -0,0 +1,30 @@ +string('latitude')->nullable()->after('foto_absen_keluar'); + $table->string('longitude')->nullable()->after('latitude'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('absensis', function (Blueprint $table) { + $table->dropColumn('latitude'); + $table->dropColumn('longitude'); + }); + } +}; diff --git a/samooapk/database/seeders/DatabaseSeeder.php b/samooapk/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..2cb2497 --- /dev/null +++ b/samooapk/database/seeders/DatabaseSeeder.php @@ -0,0 +1,19 @@ +call([ + // UserSeeder::class, + TarifPekerjaanSeeder::class, + ]); + } +} \ No newline at end of file diff --git a/samooapk/database/seeders/TarifPekerjaanSeeder.php b/samooapk/database/seeders/TarifPekerjaanSeeder.php new file mode 100644 index 0000000..ae00283 --- /dev/null +++ b/samooapk/database/seeders/TarifPekerjaanSeeder.php @@ -0,0 +1,486 @@ +insert([ + + // ========================================== + // 1. SR - Sambungan Rumah (Meteran Air) + // ========================================== + [ + 'jenis_pekerjaan' => 'sr', + 'nama_item' => 'Sambungan Rumah (SR) - Meteran Air', + 'kode_item' => 'SR-METERAN', + 'dimensi_pipa' => null, + 'tarif_per_unit' => 150000, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => true, + 'durasi_garansi_bulan' => 3, + 'deskripsi' => 'Pemasangan meteran air (SR) Rp 150.000 per unit, garansi 3 bulan', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + + // ========================================== + // 2. Pengembangan Jaringan Pipa (per meter, per dimensi) + // ========================================== + [ + 'jenis_pekerjaan' => 'pengembangan_jaringan_pipa', + 'nama_item' => 'Pengembangan Jaringan Pipa Dim 2', + 'kode_item' => 'PJP-DIM2', + 'dimensi_pipa' => '1-2', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 15000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pemasangan pipa baru diameter 2 dim, Rp 15.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pengembangan_jaringan_pipa', + 'nama_item' => 'Pengembangan Jaringan Pipa Dim 3', + 'kode_item' => 'PJP-DIM3', + 'dimensi_pipa' => '3', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 18000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pemasangan pipa baru diameter 3 dim, Rp 18.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pengembangan_jaringan_pipa', + 'nama_item' => 'Pengembangan Jaringan Pipa Dim 4', + 'kode_item' => 'PJP-DIM4', + 'dimensi_pipa' => '4', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 20000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pemasangan pipa baru diameter 4 dim, Rp 20.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pengembangan_jaringan_pipa', + 'nama_item' => 'Pengembangan Jaringan Pipa Dim 6', + 'kode_item' => 'PJP-DIM6', + 'dimensi_pipa' => '6', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 25000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pemasangan pipa baru diameter 6 dim, Rp 25.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pengembangan_jaringan_pipa', + 'nama_item' => 'Pengembangan Jaringan Pipa Dim 8', + 'kode_item' => 'PJP-DIM8', + 'dimensi_pipa' => '8', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 40000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pemasangan pipa baru diameter 8 dim, Rp 40.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pengembangan_jaringan_pipa', + 'nama_item' => 'Pengembangan Jaringan Pipa Dim 10', + 'kode_item' => 'PJP-DIM10', + 'dimensi_pipa' => '10', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 60000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pemasangan pipa baru diameter 10 dim, Rp 60.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + + // ========================================== + // 3. Pengangkatan (meteran atau gate valve) + // ========================================== + [ + 'jenis_pekerjaan' => 'pengangkatan', + 'nama_item' => 'Pengangkatan Meteran / Gate Valve', + 'kode_item' => 'ANGKAT-MGV', + 'dimensi_pipa' => null, + 'tarif_per_unit' => 200000, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pengangkatan meteran air atau gate valve terpendam, Rp 200.000 per unit', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + + // ========================================== + // 4. Pemasangan Gate Valve + Pressure + // ========================================== + [ + 'jenis_pekerjaan' => 'pemasangan_gate_valve', + 'nama_item' => 'Pemasangan Gate Valve + Pressure (Dengan Pipa Besi)', + 'kode_item' => 'GV-PAKAI-PIPA', + 'dimensi_pipa' => null, + 'tarif_per_unit' => 200000, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => true, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pemasangan gate valve + pressure dengan pipa besi, Rp 200.000', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pemasangan_gate_valve', + 'nama_item' => 'Pemasangan Gate Valve + Pressure (Tanpa Pipa Besi)', + 'kode_item' => 'GV-TANPA-PIPA', + 'dimensi_pipa' => null, + 'tarif_per_unit' => 0, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => false, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pemasangan gate valve + pressure tanpa pipa besi, GRATIS (Rp 0)', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + + // ========================================== + // 5. Gali Urug + // ========================================== + [ + 'jenis_pekerjaan' => 'gali_urug', + 'nama_item' => 'Gali Urug', + 'kode_item' => 'GALI-URUG', + 'dimensi_pipa' => null, + 'tarif_per_unit' => 200000, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Gali urug per titik, Rp 200.000/titik', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + + // ========================================== + // 6. Perbaikan Jaringan Pipa Bocor (per dimensi) + // ========================================== + [ + 'jenis_pekerjaan' => 'perbaikan_jaringan_pipa', + 'nama_item' => 'Perbaikan Pipa Bocor Dim 1-2', + 'kode_item' => 'PJP-BOR-DIM1-2', + 'dimensi_pipa' => '1-2', + 'tarif_per_unit' => 200000, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Perbaikan pipa bocor diameter 1-2 dim, Rp 200.000/titik', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'perbaikan_jaringan_pipa', + 'nama_item' => 'Perbaikan Pipa Bocor Dim 3', + 'kode_item' => 'PJP-BOR-DIM3', + 'dimensi_pipa' => '3', + 'tarif_per_unit' => 400000, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Perbaikan pipa bocor diameter 3 dim, Rp 400.000/titik', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'perbaikan_jaringan_pipa', + 'nama_item' => 'Perbaikan Pipa Bocor Dim 4', + 'kode_item' => 'PJP-BOR-DIM4', + 'dimensi_pipa' => '4', + 'tarif_per_unit' => 400000, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Perbaikan pipa bocor diameter 4 dim, Rp 400.000/titik', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'perbaikan_jaringan_pipa', + 'nama_item' => 'Perbaikan Pipa Bocor Dim 6', + 'kode_item' => 'PJP-BOR-DIM6', + 'dimensi_pipa' => '6', + 'tarif_per_unit' => 400000, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Perbaikan pipa bocor diameter 6 dim, Rp 400.000/titik', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'perbaikan_jaringan_pipa', + 'nama_item' => 'Perbaikan Pipa Bocor Dim 8', + 'kode_item' => 'PJP-BOR-DIM8', + 'dimensi_pipa' => '8', + 'tarif_per_unit' => 750000, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Perbaikan pipa bocor diameter 8 dim, Rp 750.000/titik', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'perbaikan_jaringan_pipa', + 'nama_item' => 'Perbaikan Pipa Bocor Dim 10', + 'kode_item' => 'PJP-BOR-DIM10', + 'dimensi_pipa' => '10', + 'tarif_per_unit' => 900000, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Perbaikan pipa bocor diameter 10 dim, Rp 900.000/titik', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + + // ========================================== + // 7. Pengecatan Pipa Besi (2x tarif perbaikan per dimensi) + // ========================================== + [ + 'jenis_pekerjaan' => 'pengecatan_pipa_besi', + 'nama_item' => 'Pengecatan Pipa Besi Dim 1-2', + 'kode_item' => 'CAT-DIM1-2', + 'dimensi_pipa' => '1-2', + 'tarif_per_unit' => 400000, // 2x 200.000 + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pengecatan pipa besi dim 1-2, Rp 400.000 (2x tarif perbaikan)', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pengecatan_pipa_besi', + 'nama_item' => 'Pengecatan Pipa Besi Dim 3', + 'kode_item' => 'CAT-DIM3', + 'dimensi_pipa' => '3', + 'tarif_per_unit' => 800000, // 2x 400.000 + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pengecatan pipa besi dim 3, Rp 800.000 (2x tarif perbaikan)', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pengecatan_pipa_besi', + 'nama_item' => 'Pengecatan Pipa Besi Dim 4', + 'kode_item' => 'CAT-DIM4', + 'dimensi_pipa' => '4', + 'tarif_per_unit' => 800000, // 2x 400.000 + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pengecatan pipa besi dim 4, Rp 800.000 (2x tarif perbaikan)', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pengecatan_pipa_besi', + 'nama_item' => 'Pengecatan Pipa Besi Dim 6', + 'kode_item' => 'CAT-DIM6', + 'dimensi_pipa' => '6', + 'tarif_per_unit' => 800000, // 2x 400.000 + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pengecatan pipa besi dim 6, Rp 800.000 (2x tarif perbaikan)', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pengecatan_pipa_besi', + 'nama_item' => 'Pengecatan Pipa Besi Dim 8', + 'kode_item' => 'CAT-DIM8', + 'dimensi_pipa' => '8', + 'tarif_per_unit' => 1500000, // 2x 750.000 + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pengecatan pipa besi dim 8, Rp 1.500.000 (2x tarif perbaikan)', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pengecatan_pipa_besi', + 'nama_item' => 'Pengecatan Pipa Besi Dim 10', + 'kode_item' => 'CAT-DIM10', + 'dimensi_pipa' => '10', + 'tarif_per_unit' => 1800000, // 2x 900.000 + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pengecatan pipa besi dim 10, Rp 1.800.000 (2x tarif perbaikan)', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + + // ========================================== + // 8. Penyempurnaan Jaringan Pipa (sama seperti pengembangan) + // ========================================== + [ + 'jenis_pekerjaan' => 'penyempurnaan_jaringan_pipa', + 'nama_item' => 'Penyempurnaan Jaringan Pipa Dim 2', + 'kode_item' => 'SEMPURNA-DIM2', + 'dimensi_pipa' => '1-2', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 15000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Penyempurnaan jaringan pipa dim 2 (pipa kecil ke besar), Rp 15.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'penyempurnaan_jaringan_pipa', + 'nama_item' => 'Penyempurnaan Jaringan Pipa Dim 3', + 'kode_item' => 'SEMPURNA-DIM3', + 'dimensi_pipa' => '3', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 18000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Penyempurnaan jaringan pipa dim 3, Rp 18.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'penyempurnaan_jaringan_pipa', + 'nama_item' => 'Penyempurnaan Jaringan Pipa Dim 4', + 'kode_item' => 'SEMPURNA-DIM4', + 'dimensi_pipa' => '4', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 20000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Penyempurnaan jaringan pipa dim 4, Rp 20.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'penyempurnaan_jaringan_pipa', + 'nama_item' => 'Penyempurnaan Jaringan Pipa Dim 6', + 'kode_item' => 'SEMPURNA-DIM6', + 'dimensi_pipa' => '6', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 25000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Penyempurnaan jaringan pipa dim 6, Rp 25.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'penyempurnaan_jaringan_pipa', + 'nama_item' => 'Penyempurnaan Jaringan Pipa Dim 8', + 'kode_item' => 'SEMPURNA-DIM8', + 'dimensi_pipa' => '8', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 40000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Penyempurnaan jaringan pipa dim 8, Rp 40.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'penyempurnaan_jaringan_pipa', + 'nama_item' => 'Penyempurnaan Jaringan Pipa Dim 10', + 'kode_item' => 'SEMPURNA-DIM10', + 'dimensi_pipa' => '10', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 60000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Penyempurnaan jaringan pipa dim 10, Rp 60.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + ]); + } +} \ No newline at end of file diff --git a/samooapk/debug_penggajian.php b/samooapk/debug_penggajian.php new file mode 100644 index 0000000..ca918e8 --- /dev/null +++ b/samooapk/debug_penggajian.php @@ -0,0 +1,38 @@ +make('Illuminate\Contracts\Console\Kernel')->bootstrap(); + +use App\Models\Penugasan; +use App\Models\Absensi; + +echo "=== SEMUA PENUGASAN (termasuk semua status) ===\n"; +$all = Penugasan::withTrashed()->get(); +echo "Total penugasan (inc soft-deleted): " . $all->count() . "\n\n"; + +$statuses = $all->groupBy('status_pekerjaan'); +echo "Per status:\n"; +foreach ($statuses as $status => $items) { + echo " {$status}: " . $items->count() . "\n"; +} + +echo "\n=== PENUGASAN YANG BELUM SOFT-DELETED ===\n"; +$active = Penugasan::all(); +echo "Total: " . $active->count() . "\n"; +foreach ($active as $p) { + echo " ID: {$p->id_penugasan}, Teknisi: {$p->id_teknisi}, Status: {$p->status_pekerjaan}\n"; + echo " Jenis: {$p->jenis_pekerjaan}, Lokasi: {$p->alamat_lokasi}\n"; + echo " total_nilai_pekerjaan: {$p->total_nilai_pekerjaan}\n"; + echo " tanggal_diberikan: {$p->tanggal_diberikan}\n"; + echo " tanggal_diselesaikan: {$p->tanggal_diselesaikan}\n"; + echo " ---\n"; +} + +echo "\n=== ABSENSI ===\n"; +$absensi = Absensi::all(); +echo "Total absensi records: " . $absensi->count() . "\n"; +$hadir = Absensi::where('status', 'hadir')->count(); +echo "Status hadir: {$hadir}\n"; + +echo "\nDone.\n"; diff --git a/samooapk/laravel.zip b/samooapk/laravel.zip new file mode 100644 index 0000000..d833791 Binary files /dev/null and b/samooapk/laravel.zip differ diff --git a/samooapk/laravel/.editorconfig b/samooapk/laravel/.editorconfig new file mode 100644 index 0000000..8f0de65 --- /dev/null +++ b/samooapk/laravel/.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/samooapk/laravel/.env.example b/samooapk/laravel/.env.example new file mode 100644 index 0000000..ea0665b --- /dev/null +++ b/samooapk/laravel/.env.example @@ -0,0 +1,59 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +LOG_CHANNEL=stack +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=laravel +DB_USERNAME=root +DB_PASSWORD= + +BROADCAST_DRIVER=log +CACHE_DRIVER=file +FILESYSTEM_DISK=local +QUEUE_CONNECTION=sync +SESSION_DRIVER=file +SESSION_LIFETIME=120 + +MEMCACHED_HOST=127.0.0.1 + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=smtp +MAIL_HOST=mailpit +MAIL_PORT=1025 +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 + +PUSHER_APP_ID= +PUSHER_APP_KEY= +PUSHER_APP_SECRET= +PUSHER_HOST= +PUSHER_PORT=443 +PUSHER_SCHEME=https +PUSHER_APP_CLUSTER=mt1 + +VITE_APP_NAME="${APP_NAME}" +VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" +VITE_PUSHER_HOST="${PUSHER_HOST}" +VITE_PUSHER_PORT="${PUSHER_PORT}" +VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" +VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" diff --git a/samooapk/laravel/.gitattributes b/samooapk/laravel/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/samooapk/laravel/.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/samooapk/laravel/.gitignore b/samooapk/laravel/.gitignore new file mode 100644 index 0000000..7fe978f --- /dev/null +++ b/samooapk/laravel/.gitignore @@ -0,0 +1,19 @@ +/.phpunit.cache +/node_modules +/public/build +/public/hot +/public/storage +/storage/*.key +/vendor +.env +.env.backup +.env.production +.phpunit.result.cache +Homestead.json +Homestead.yaml +auth.json +npm-debug.log +yarn-error.log +/.fleet +/.idea +/.vscode diff --git a/samooapk/laravel/README.md b/samooapk/laravel/README.md new file mode 100644 index 0000000..1a4c26b --- /dev/null +++ b/samooapk/laravel/README.md @@ -0,0 +1,66 @@ +

Laravel Logo

+ +

+Build Status +Total Downloads +Latest Stable Version +License +

+ +## 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/samooapk/laravel/app/Console/Commands/MigrateOldCatatanData.php b/samooapk/laravel/app/Console/Commands/MigrateOldCatatanData.php new file mode 100644 index 0000000..21e6b42 --- /dev/null +++ b/samooapk/laravel/app/Console/Commands/MigrateOldCatatanData.php @@ -0,0 +1,67 @@ +whereNotNull('catatan_admin') + ->get(); + + $this->info("Ditemukan {$penugasans->count()} data lama yang perlu dimigrasi..."); + $updated = 0; + + foreach ($penugasans as $p) { + $catatan = $p->catatan_admin; + $nama = null; + $alamat = null; + $noSamb = null; + + // ── Cari Nama ────────────────────────────────── + // Pola: "Nama : ...", "Nama DR ...", "Nama: ..." + if (preg_match('/Nama\s*[:\-]?\s*(.+?)(?=Alamat|Pekerjaan|No\.?|$)/is', $catatan, $m)) { + $nama = trim($m[1]); + } + + // ── Cari Alamat ───────────────────────────────── + // Pola: "Alamat: ...", "Alamat BR. ..." + if (preg_match('/Alamat\s*[:\-]?\s*(.+?)(?=Pekerjaan|No\.?|Nama|$)/is', $catatan, $m)) { + $alamat = trim($m[1]); + } + + // ── Cari No Sambungan ─────────────────────────── + // Pola: "no sambungan 0032", "No sambungannya 0008", "no samb 0032" + if (preg_match('/no\.?\s*samb(?:ungan(?:nya)?)?\s*[:\-]?\s*([0-9]+)/is', $catatan, $m)) { + $noSamb = trim($m[1]); + } + + // Bersihkan trailing punct dari hasil parsing + $nama = $nama ? rtrim($nama, ' ,;.') : null; + $alamat = $alamat ? rtrim($alamat, ' ,;.') : null; + + // Hanya update kalau minimal ada salah satu yang berhasil di-parse + if ($nama || $alamat || $noSamb) { + $p->update([ + 'nama_pelanggan' => $nama, + 'alamat_lokasi' => $alamat, + 'no_sambungan' => $noSamb, + ]); + $updated++; + $this->line(" ✅ ID #{$p->id_penugasan} → Nama: $nama | Alamat: $alamat | No: $noSamb"); + } else { + $this->line(" ⚠️ ID #{$p->id_penugasan} → Format tidak dikenali: \"{$catatan}\""); + } + } + + $this->info("\n✅ Selesai! $updated dari {$penugasans->count()} data berhasil dimigrasi."); + return 0; + } +} diff --git a/samooapk/laravel/app/Console/Kernel.php b/samooapk/laravel/app/Console/Kernel.php new file mode 100644 index 0000000..e6b9960 --- /dev/null +++ b/samooapk/laravel/app/Console/Kernel.php @@ -0,0 +1,27 @@ +command('inspire')->hourly(); + } + + /** + * Register the commands for the application. + */ + protected function commands(): void + { + $this->load(__DIR__.'/Commands'); + + require base_path('routes/console.php'); + } +} diff --git a/samooapk/laravel/app/Exceptions/Handler.php b/samooapk/laravel/app/Exceptions/Handler.php new file mode 100644 index 0000000..56af264 --- /dev/null +++ b/samooapk/laravel/app/Exceptions/Handler.php @@ -0,0 +1,30 @@ + + */ + protected $dontFlash = [ + 'current_password', + 'password', + 'password_confirmation', + ]; + + /** + * Register the exception handling callbacks for the application. + */ + public function register(): void + { + $this->reportable(function (Throwable $e) { + // + }); + } +} diff --git a/samooapk/laravel/app/Http/Controllers/AbsensiController.php b/samooapk/laravel/app/Http/Controllers/AbsensiController.php new file mode 100644 index 0000000..d3b3c64 --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/AbsensiController.php @@ -0,0 +1,252 @@ +get(); + + // Query dasar untuk data utama + $query = Absensi::with('teknisi'); + + // Filter Rentang Tanggal + if ($request->filled('start_date') && $request->filled('end_date')) { + $query->whereBetween('tanggal', [$request->start_date, $request->end_date]); + } elseif ($request->filled('start_date')) { + $query->whereDate('tanggal', '>=', $request->start_date); + } elseif ($request->filled('end_date')) { + $query->whereDate('tanggal', '<=', $request->end_date); + } + + // Filter Teknisi + if ($request->filled('teknisi')) { + $query->where('id_teknisi', $request->teknisi); + } + + // Hitung statistik berdasarkan filter di atas (sebelum filter status) + $counts = [ + 'total' => (clone $query)->count(), + 'hadir' => (clone $query)->where('status', 'hadir')->count(), + 'izin' => (clone $query)->whereIn('status', ['izin', 'sakit'])->count(), + ]; + + // Apply filter status untuk tabel + if ($request->query('status') === 'izin') { + $query->whereIn('status', ['izin', 'sakit']); + } else { + $query->filterByStatus($request->query('status')); + } + + $absensis = $query->orderBy('tanggal', 'desc') + ->orderBy('jam_masuk', 'desc') + ->paginate(15); + + return view('Admin.KelolaTeknisi.Absensi', compact('absensis', 'teknisis', 'counts')); + } + + /** + * Menampilkan detail data absensi spesifik. + */ + public function show($id) + { + $absensi = Absensi::with('teknisi')->where('id_absensi', $id)->first(); + + if (!$absensi) { + return response()->json([ + 'error' => 'Data absensi tidak ditemukan.' + ], 404); + } + + // Debug log + \Log::info('=== DEBUG ABSENSI SHOW ==='); + \Log::info('Absensi ID: ' . $absensi->id_absensi); + \Log::info('Foto Masuk Path: ' . ($absensi->foto_absen_masuk ?? 'NULL')); + \Log::info('Foto Keluar Path: ' . ($absensi->foto_absen_keluar ?? 'NULL')); + + // Cek apakah file benar-benar ada + if ($absensi->foto_absen_masuk) { + $exists = Storage::disk('public')->exists($absensi->foto_absen_masuk); + \Log::info('Foto Masuk Exists: ' . ($exists ? 'YES' : 'NO')); + \Log::info('Full Path: ' . Storage::disk('public')->path($absensi->foto_absen_masuk)); + } + + if ($absensi->foto_absen_keluar) { + $exists = Storage::disk('public')->exists($absensi->foto_absen_keluar); + \Log::info('Foto Keluar Exists: ' . ($exists ? 'YES' : 'NO')); + \Log::info('Full Path: ' . Storage::disk('public')->path($absensi->foto_absen_keluar)); + } + + // Generate URL yang benar + $fotoMasukUrl = null; + $fotoKeluarUrl = null; + + if ($absensi->foto_absen_masuk && Storage::disk('public')->exists($absensi->foto_absen_masuk)) { + $fotoMasukUrl = Storage::url($absensi->foto_absen_masuk); + \Log::info('Generated Foto Masuk URL: ' . $fotoMasukUrl); + } + + if ($absensi->foto_absen_keluar && Storage::disk('public')->exists($absensi->foto_absen_keluar)) { + $fotoKeluarUrl = Storage::url($absensi->foto_absen_keluar); + \Log::info('Generated Foto Keluar URL: ' . $fotoKeluarUrl); + } + + $data = [ + 'id' => $absensi->id_absensi, + 'tanggal' => $absensi->tanggal instanceof \Carbon\Carbon + ? $absensi->tanggal->format('d/m/Y') + : Carbon::parse($absensi->tanggal)->format('d/m/Y'), + 'tanggal_full' => $absensi->tanggal instanceof \Carbon\Carbon + ? $absensi->tanggal->isoFormat('dddd, D MMMM YYYY') + : Carbon::parse($absensi->tanggal)->isoFormat('dddd, D MMMM YYYY'), + 'jam_masuk' => $absensi->jam_masuk + ? Carbon::parse($absensi->jam_masuk)->format('H:i') + : '-', + 'jam_keluar' => $absensi->jam_keluar + ? Carbon::parse($absensi->jam_keluar)->format('H:i') + : '-', + 'durasi_kerja' => $absensi->durasi_kerja_formatted ?? '00:00', + 'status' => $absensi->status, + 'status_formatted' => ucfirst($absensi->status), + 'status_badge_class' => $absensi->status_badge_class ?? 'badge-secondary', + 'is_terlambat' => $absensi->is_terlambat ?? false, + 'keterangan' => $absensi->keterangan ?? '-', + 'teknisi' => [ + 'id' => $absensi->teknisi ? $absensi->teknisi->id_teknisi : null, + 'nama' => $absensi->teknisi ? $absensi->teknisi->nama : 'Teknisi tidak ditemukan', + 'email' => ($absensi->teknisi && $absensi->teknisi->email) + ? $absensi->teknisi->email + : 'Email tidak tersedia', + ], + 'foto_masuk_url' => $fotoMasukUrl, + 'foto_keluar_url' => $fotoKeluarUrl, + 'kategori_kerja' => $absensi->kategori_kerja, + 'latitude' => $absensi->latitude ?? '0', + 'longitude' => $absensi->longitude ?? '0', + ]; + + \Log::info('Response Data: ' . json_encode($data)); + \Log::info('=== END DEBUG ==='); + + return response()->json([ + 'success' => true, + 'data' => $data + ]); + } + + /** + * Mendapatkan statistik absensi + */ + public function getStatistik(Request $request) + { + $id_teknisi = $request->id_teknisi; + $start = $request->start_date; + $end = $request->end_date; + + // Ambil data mentah pakai Query Builder agar lebih pasti + $data = \DB::table('absensis') + ->whereBetween('tanggal', [$start . ' 00:00:00', $end . ' 23:59:59']) + ->get(); + + $stats = ['hadir' => 0, 'terlambat' => 0, 'izin' => 0, 'sakit' => 0]; + $trendGroups = []; + $rankGroups = []; + $weekly = ['Sen'=>0, 'Sel'=>0, 'Rab'=>0, 'Kam'=>0, 'Jum'=>0]; + $dayMap = [1=>'Min', 2=>'Sen', 3=>'Sel', 4=>'Rab', 5=>'Kam', 6=>'Jum', 7=>'Sab']; + + foreach ($data as $d) { + $status = strtolower($d->status); + $tgl = \Carbon\Carbon::parse($d->tanggal); + + // 1. Filter Teknisi untuk Statistik Utama + if (!$id_teknisi || $d->id_teknisi == $id_teknisi) { + if (isset($stats[$status])) $stats[$status]++; + $jam = $d->jam_masuk ? \Carbon\Carbon::parse($d->jam_masuk)->format('H:i') : ''; + if ($jam && ($jam < '07:00' || $jam > '18:00')) $stats['terlambat']++; + } + + // 2. Trend Bulanan (Gunakan semua data biar grafiknya penuh) + $week = 'Minggu ' . ceil($tgl->day / 7); + if (!isset($trendGroups[$week])) { + $trendGroups[$week] = ['label' => $week, 'hadir' => 0, 'urgent' => 0]; + } + if ($status == 'hadir') $trendGroups[$week]['hadir']++; + + // 3. Ranking Data (Group by Teknisi) + if (!isset($rankGroups[$d->id_teknisi])) { + $tek = \DB::table('teknisis')->where('id_teknisi', $d->id_teknisi)->first(); + $rankGroups[$d->id_teknisi] = [ + 'name' => $tek ? $tek->nama : 'Unknown', + 'init' => $tek ? strtoupper(substr($tek->nama, 0, 2)) : '??', + 'total' => 0, 'hadir' => 0 + ]; + } + $rankGroups[$d->id_teknisi]['total']++; + if ($status == 'hadir') $rankGroups[$d->id_teknisi]['hadir']++; + + // 4. Weekly Data + $dayName = $dayMap[$tgl->dayOfWeek + 1] ?? ''; + if (isset($weekly[$dayName]) && $status == 'hadir') $weekly[$dayName]++; + } + + // Finalisasi Ranking + $rankings = collect($rankGroups)->map(function($r) { + $r['pct'] = $r['total'] > 0 ? round(($r['hadir'] / $r['total']) * 100) : 0; + return $r; + })->sortByDesc('pct')->values()->take(5); + + // Finalisasi Trend (Urutkan Minggu 1, 2, dst) + ksort($trendGroups); + $trend = array_values($trendGroups); + + return response()->json([ + 'success' => true, + 'data' => array_merge($stats, [ + 'trend' => $trend, + 'rankings' => $rankings, + 'weekly' => $weekly + ]) + ]); + } + + /** + * Memperbarui data absensi (digunakan oleh Admin). + */ + public function update(Request $request, $id) + { + $request->validate([ + 'jam_masuk' => 'nullable', + 'jam_keluar' => 'nullable', + 'status' => 'required|in:hadir,izin,sakit', + 'keterangan' => 'nullable|string' + ]); + + $absensi = Absensi::findOrFail($id); + $tgl = \Carbon\Carbon::parse($absensi->tanggal)->format('Y-m-d'); + + // Update fields + $absensi->jam_masuk = $request->jam_masuk ? $tgl . ' ' . $request->jam_masuk : null; + $absensi->jam_keluar = $request->jam_keluar ? $tgl . ' ' . $request->jam_keluar : null; + $absensi->status = $request->status; + $absensi->keterangan = $request->keterangan; + + $absensi->save(); + + return response()->json([ + 'success' => true, + 'message' => 'Data absensi berhasil diperbarui.' + ]); + } + +} \ No newline at end of file diff --git a/samooapk/laravel/app/Http/Controllers/AkunTeknisiController.php b/samooapk/laravel/app/Http/Controllers/AkunTeknisiController.php new file mode 100644 index 0000000..0c68fe4 --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/AkunTeknisiController.php @@ -0,0 +1,408 @@ +get(); + $teknisis = Teknisi::whereNotIn('id_teknisi', + AkunTeknisi::pluck('id_teknisi'))->get(); + + return view('Admin.KelolaTeknisi.AkunTeknisi', compact('akunTeknisis', 'teknisis')); + } + + /** + * Tampilkan form untuk membuat akun teknisi baru. + */ + public function create() + { + $teknisi = Teknisi::all(); + return view('Admin.KelolaTeknisi.create-akun', compact('teknisi')); + } + + /** + * Simpan akun teknisi baru ke database. + */ + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'id_teknisi' => 'required|exists:teknisis,id_teknisi|unique:akun_teknisis,id_teknisi', + 'username' => 'required|string|max:255|unique:akun_teknisis,username', + 'password' => 'required|string|min:6', + 'status' => 'required|in:aktif,tidak_aktif', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'errors' => $validator->errors() + ], 422); + } + + try { + AkunTeknisi::create([ + 'id_teknisi' => $request->id_teknisi, + 'username' => $request->username, + 'password' => Hash::make($request->password), + 'password_plain' => $request->password, + 'status' => $request->status, + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Akun teknisi berhasil dibuat!' + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal membuat akun teknisi: ' . $e->getMessage() + ], 500); + } + } + + /** + * Tampilkan detail akun teknisi tertentu. + */ + public function show($id) + { + try { + $akunTeknisi = AkunTeknisi::with('teknisi')->findOrFail($id); + return response()->json($akunTeknisi); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Akun teknisi tidak ditemukan' + ], 404); + } + } + + /** + * Tampilkan form untuk mengedit akun teknisi. + */ + public function edit($id) + { + try { + $akunTeknisi = AkunTeknisi::with('teknisi')->findOrFail($id); + return response()->json($akunTeknisi); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Akun teknisi tidak ditemukan' + ], 404); + } + } + + /** + * Update akun teknisi di database. + */ + public function update(Request $request, $id) + { + try { + $akunTeknisi = AkunTeknisi::findOrFail($id); + + $validator = Validator::make($request->all(), [ + 'id_teknisi' => 'required|exists:teknisis,id_teknisi|unique:akun_teknisis,id_teknisi,' . $id . ',id_akun_teknisi', + 'username' => 'required|string|max:255|unique:akun_teknisis,username,' . $id . ',id_akun_teknisi', + 'password' => 'nullable|string|min:6', + 'status' => 'required|in:aktif,tidak_aktif', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'errors' => $validator->errors() + ], 422); + } + + $updateData = [ + 'id_teknisi' => $request->id_teknisi, + 'username' => $request->username, + 'status' => $request->status, + ]; + + // Hanya update password jika diisi + if ($request->filled('password')) { + $updateData['password'] = Hash::make($request->password); + $updateData['password_plain'] = $request->password; + } + + $akunTeknisi->update($updateData); + + return response()->json([ + 'success' => true, + 'message' => 'Akun teknisi berhasil diupdate!' + ]); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal update akun teknisi: ' . $e->getMessage() + ], 500); + } + } + + /** + * Hapus akun teknisi dari database. + */ + public function destroy($id) + { + try { + $akunTeknisi = AkunTeknisi::findOrFail($id); + $akunTeknisi->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Akun teknisi berhasil dihapus!' + ]); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal hapus akun teknisi: ' . $e->getMessage() + ], 500); + } + } + + /** + * Login untuk teknisi (Mobile App). + */ + public function login(Request $request) + { + $validator = Validator::make($request->all(), [ + 'username' => 'required|string', + 'password' => 'required|string', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed', + 'errors' => $validator->errors() + ], 422); + } + + try { + // Cari akun teknisi + $akun = AkunTeknisi::where('username', $request->username) + ->where('status', 'aktif') + ->with('teknisi') + ->first(); + + // Cek kredensial + if (!$akun || !Hash::check($request->password, $akun->password)) { + return response()->json([ + 'success' => false, + 'message' => 'Username atau password salah' + ], 401); + } + + // Generate JWT token + $token = auth('api')->login($akun); + + if (!$token) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal membuat token' + ], 500); + } + + return response()->json([ + 'success' => true, + 'message' => 'Login berhasil', + 'access_token' => $token, + 'token_type' => 'bearer', + 'expires_in' => auth('api')->factory()->getTTL() * 60, + 'user' => [ + 'id_akun_teknisi' => $akun->id_akun_teknisi, + 'username' => $akun->username, + 'status' => $akun->status, + 'teknisi' => $akun->teknisi + ] + ]); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Terjadi kesalahan saat login: ' . $e->getMessage() + ], 500); + } + } + + /** + * Logout teknisi. + */ + public function logout() + { + try { + auth('api')->logout(); + + return response()->json([ + 'success' => true, + 'message' => 'Logout berhasil' + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Terjadi kesalahan saat logout: ' . $e->getMessage() + ], 500); + } + } + + /** + * Get profile teknisi yang sedang login. + */ + public function me() + { + try { + $akun = auth('api')->user(); + + if (!$akun) { + return response()->json([ + 'success' => false, + 'message' => 'User tidak ditemukan' + ], 404); + } + + // Load relasi teknisi + $akun->load('teknisi'); + + return response()->json([ + 'success' => true, + 'message' => 'Data berhasil diambil', + 'data' => [ + 'id_akun_teknisi' => $akun->id_akun_teknisi, + 'username' => $akun->username, + 'status' => $akun->status, + 'teknisi' => $akun->teknisi + ] + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Terjadi kesalahan: ' . $e->getMessage() + ], 500); + } + } + + /** + * Refresh JWT token. + */ + public function refresh() + { + try { + $newToken = auth('api')->refresh(); + + return response()->json([ + 'success' => true, + 'message' => 'Token berhasil di-refresh', + 'access_token' => $newToken, + 'token_type' => 'bearer', + 'expires_in' => auth('api')->factory()->getTTL() * 60 + ]); + } catch (JWTException $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal refresh token: ' . $e->getMessage() + ], 500); + } + } + + /** + * Change password teknisi. + */ + public function changePassword(Request $request) + { + $validator = Validator::make($request->all(), [ + 'password_lama' => 'required|string', + 'password_baru' => 'required|string|min:6|confirmed', + ], [ + 'password_baru.confirmed' => 'Konfirmasi password tidak sesuai', + 'password_baru.min' => 'Password baru minimal 6 karakter', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validasi gagal', + 'errors' => $validator->errors() + ], 422); + } + + try { + $akun = auth('api')->user(); + + // Cek password lama + if (!Hash::check($request->password_lama, $akun->password)) { + return response()->json([ + 'success' => false, + 'message' => 'Password lama tidak sesuai' + ], 401); + } + + // Update password + $akun->update([ + 'password' => Hash::make($request->password_baru) + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Password berhasil diubah' + ]); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengubah password: ' . $e->getMessage() + ], 500); + } + } + + /** + * Update status akun teknisi. + */ + public function updateStatus(Request $request, $id) + { + try { + $akunTeknisi = AkunTeknisi::findOrFail($id); + + $validator = Validator::make($request->all(), [ + 'status' => 'required|in:aktif,tidak_aktif', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'errors' => $validator->errors() + ], 422); + } + + $akunTeknisi->update([ + 'status' => $request->status + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Status akun teknisi berhasil diupdate!' + ]); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal update status: ' . $e->getMessage() + ], 500); + } + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Http/Controllers/Api/AbsensiApiController.php b/samooapk/laravel/app/Http/Controllers/Api/AbsensiApiController.php new file mode 100644 index 0000000..1db7105 --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/Api/AbsensiApiController.php @@ -0,0 +1,573 @@ +all()); + + $status = $request->input('status', 'hadir'); + + $rules = [ + 'id_teknisi' => 'required|exists:teknisis,id_teknisi', + 'status' => 'nullable|in:hadir,izin,sakit', + 'keterangan' => 'nullable|string|max:255', + 'latitude' => 'nullable|string', + 'longitude' => 'nullable|string', + ]; + + if ($status === 'hadir') { + $rules['foto_absen_masuk'] = 'required|image|mimes:jpeg,png,jpg,gif|max:2048'; + } else { + $rules['foto_absen_masuk'] = 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048'; + } + + if ($status === 'izin') { + $rules['keterangan'] = 'required|string|max:255'; + } + + $validator = Validator::make($request->all(), $rules, [ + 'foto_absen_masuk.required' => 'Foto wajib untuk status Hadir', + 'keterangan.required' => 'Keterangan wajib untuk status Izin', + ]); + + if ($validator->fails()) { + Log::error('Validation failed:', $validator->errors()->toArray()); + return response()->json([ + 'success' => false, + 'message' => 'Validasi gagal', + 'errors' => $validator->errors() + ], 422); + } + + try { + // Cek apakah ada sesi yang masih aktif (belum absen keluar) + $activeAbsen = Absensi::where('id_teknisi', $request->id_teknisi) + ->whereNull('jam_keluar') + ->whereDate('tanggal', Carbon::today()) + ->first(); + + if ($activeAbsen) { + return response()->json([ + 'success' => false, + 'message' => 'Anda masih memiliki sesi absen yang aktif' + ], 400); + } + + $data = [ + 'id_teknisi' => $request->id_teknisi, + 'tanggal' => Carbon::now('Asia/Jakarta')->toDateString(), + 'jam_masuk' => $status === 'hadir' ? Carbon::now('Asia/Jakarta') : null, + 'status' => $status, + 'keterangan' => $request->keterangan, + 'latitude' => $request->latitude, + 'longitude' => $request->longitude, + ]; + + if ($request->hasFile('foto_absen_masuk')) { + $data['foto_absen_masuk'] = $request->file('foto_absen_masuk') + ->store('absensi-masuk', 'public'); + } + + $absensi = Absensi::create($data); + $absensi->load('teknisi'); + + return response()->json([ + 'success' => true, + 'message' => 'Absen masuk berhasil dicatat', + 'data' => $absensi + ], 201); + + } catch (\Exception $e) { + Log::error('Error in absenMasuk: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Gagal melakukan absen masuk', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Absen keluar untuk teknisi dengan support status. + */ + public function absenKeluar(Request $request) + { + Log::info('Absen Keluar Request:', $request->all()); + + $status = $request->input('status', 'hadir'); + + $rules = [ + 'id_teknisi' => 'required|exists:teknisis,id_teknisi', + 'status' => 'nullable|in:hadir,izin,sakit', + 'keterangan' => 'nullable|string|max:255', + 'latitude' => 'nullable|string', + 'longitude' => 'nullable|string', + ]; + + if ($status === 'hadir') { + $rules['foto_absen_keluar'] = 'required|image|mimes:jpeg,png,jpg,gif|max:2048'; + } else { + $rules['foto_absen_keluar'] = 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048'; + } + + if ($status === 'izin') { + $rules['keterangan'] = 'required|string|max:255'; + } + + $validator = Validator::make($request->all(), $rules, [ + 'foto_absen_keluar.required' => 'Foto wajib untuk status Hadir', + 'keterangan.required' => 'Keterangan wajib untuk status Izin', + ]); + + if ($validator->fails()) { + Log::error('Validation failed:', $validator->errors()->toArray()); + return response()->json([ + 'success' => false, + 'message' => 'Validasi gagal', + 'errors' => $validator->errors() + ], 422); + } + + try { + // Cari sesi terbaru yang belum absen keluar + $absensi = Absensi::where('id_teknisi', $request->id_teknisi) + ->whereNull('jam_keluar') + ->orderBy('id_absensi', 'desc') + ->first(); + + if (!$absensi) { + return response()->json([ + 'success' => false, + 'message' => 'Tidak ada sesi absen aktif yang ditemukan' + ], 400); + } + + $data = ['jam_keluar' => Carbon::now('Asia/Jakarta')]; + + if ($request->has('status')) { + $data['status'] = $status; + } + + if ($request->has('keterangan')) { + $data['keterangan'] = $absensi->keterangan + ? $absensi->keterangan . ' | ' . $request->keterangan + : $request->keterangan; + } + + if ($request->has('latitude')) { + $data['latitude'] = $request->latitude; + } + + if ($request->has('longitude')) { + $data['longitude'] = $request->longitude; + } + + if ($request->hasFile('foto_absen_keluar')) { + $data['foto_absen_keluar'] = $request->file('foto_absen_keluar') + ->store('absensi-keluar', 'public'); + } + + $absensi->update($data); + $absensi->load('teknisi'); + + return response()->json([ + 'success' => true, + 'message' => 'Absen keluar berhasil dicatat', + 'data' => $absensi + ], 200); + + } catch (\Exception $e) { + Log::error('Error in absenKeluar: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Gagal melakukan absen keluar', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Mengecek status absensi teknisi hari ini. + */ + public function checkStatus($id_teknisi) + { + try { + $teknisi = Teknisi::where('id_teknisi', $id_teknisi)->first(); + + if (!$teknisi) { + return response()->json([ + 'success' => false, + 'message' => 'Teknisi tidak ditemukan' + ], 404); + } + + // Ambil sesi terbaru hari ini + $absensi = Absensi::where('id_teknisi', $id_teknisi) + ->whereDate('tanggal', Carbon::today()) + ->orderBy('id_absensi', 'desc') + ->first(); + + $status = [ + 'sudah_absen_masuk' => false, + 'sudah_absen_keluar' => false, + 'data_absensi' => null, + ]; + + if ($absensi) { + $status['sudah_absen_masuk'] = !empty($absensi->jam_masuk) && empty($absensi->jam_keluar) && $absensi->status === 'hadir'; + $status['sudah_absen_keluar'] = !empty($absensi->jam_keluar); + $status['data_absensi'] = [ + 'jam_masuk' => $absensi->jam_masuk, + 'jam_keluar' => $absensi->jam_keluar, + 'jam_masuk_formatted' => $absensi->jam_masuk_formatted, + 'jam_keluar_formatted' => $absensi->jam_keluar_formatted, + 'durasi_kerja_formatted' => $absensi->durasi_kerja_formatted, + 'status' => $absensi->status, + 'keterangan' => $absensi->keterangan, + 'lokasi_masuk' => $absensi->lokasi_masuk ?? '-', + 'lokasi_valid' => $absensi->lokasi_valid ?? false, + 'latitude' => $absensi->latitude, + 'longitude' => $absensi->longitude, + 'foto_absen_masuk' => $absensi->foto_absen_masuk, + 'foto_absen_keluar' => $absensi->foto_absen_keluar, + ]; + } + + return response()->json([ + 'success' => true, + 'message' => 'Status absensi berhasil diambil', + 'data' => $status + ], 200); + + } catch (\Exception $e) { + Log::error('Error in checkStatus: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengecek status absensi', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Mendapatkan riwayat absensi teknisi per bulan + * dengan field yang sudah diformat untuk blade & Flutter. + */ + public function riwayat(Request $request) + { + try { + $id_teknisi = $request->query('id_teknisi'); + + if (!$id_teknisi) { + return response()->json([ + 'success' => false, + 'message' => 'id_teknisi diperlukan' + ], 400); + } + + $query = Absensi::where('id_teknisi', $id_teknisi); + + if ($request->has('bulan') && $request->has('tahun')) { + $query->filterByMonth($request->bulan, $request->tahun); + } + + $absensis = $query->orderBy('tanggal', 'desc')->get(); + + // ── Transform: kirim field yang sudah diformat ────────────── + $data = $absensis->map(function ($absensi) { + // Hitung menit telat (jadwal masuk 08:00) + $menitTelat = 0; + $terlambat = false; + + if ($absensi->jam_masuk && $absensi->status === 'hadir') { + $jamMasuk = Carbon::parse($absensi->jam_masuk)->setTimezone('Asia/Jakarta'); + $jamJadwal = Carbon::parse( + $absensi->tanggal->format('Y-m-d') . ' 08:00:00' + )->setTimezone('Asia/Jakarta'); + + if ($jamMasuk->gt($jamJadwal)) { + $terlambat = true; + $menitTelat = $jamMasuk->diffInMinutes($jamJadwal); + } + } + + return [ + 'tanggal' => $absensi->tanggal + ? $absensi->tanggal->format('Y-m-d') + : null, + 'status' => $absensi->status, + 'jam_masuk_formatted' => $absensi->jam_masuk_formatted, + 'jam_keluar_formatted' => $absensi->jam_keluar_formatted, + 'durasi_kerja_formatted' => $absensi->durasi_kerja_formatted, + 'terlambat' => $terlambat, + 'menit_telat' => $menitTelat, + 'keterangan' => $absensi->keterangan, + 'latitude' => $absensi->latitude, + 'longitude' => $absensi->longitude, + ]; + }); + + return response()->json([ + 'success' => true, + 'message' => 'Riwayat absensi berhasil diambil', + 'data' => $data + ], 200); + + } catch (\Exception $e) { + Log::error('Error in riwayat: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil riwayat absensi', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Mendapatkan statistik absensi. + */ + public function statistik(Request $request) + { + try { + $startDate = $request->input('start_date'); + $endDate = $request->input('end_date'); + $idTeknisi = $request->input('id_teknisi'); + + $query = Absensi::query(); + + if ($startDate && $endDate) { + $query->whereBetween('tanggal', [$startDate, $endDate]); + } + + if ($idTeknisi) { + $query->where('id_teknisi', $idTeknisi); + } + + $statistik = [ + 'total' => $query->count(), + 'hadir' => (clone $query)->where('status', 'hadir')->count(), + 'sakit' => (clone $query)->where('status', 'sakit')->count(), + 'izin' => (clone $query)->where('status', 'izin')->count(), + ]; + + return response()->json([ + 'success' => true, + 'message' => 'Statistik absensi berhasil diambil', + 'data' => $statistik + ], 200); + + } catch (\Exception $e) { + Log::error('Error in statistik: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil statistik absensi', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Mendapatkan daftar status absensi yang tersedia. + */ + public function getStatusOptions() + { + return response()->json([ + 'success' => true, + 'message' => 'Daftar status absensi berhasil diambil', + 'data' => [ + 'hadir' => 'Hadir', + 'izin' => 'Izin', + 'sakit' => 'Sakit', + ] + ], 200); + } + + /** + * Mendapatkan rekap absensi bulanan teknisi. + */ + public function rekap(Request $request) + { + try { + $id_teknisi = $request->query('id_teknisi'); + $bulan = (int) $request->query('bulan', date('n')); + $tahun = (int) $request->query('tahun', date('Y')); + + if (!$id_teknisi) { + return response()->json([ + 'success' => false, + 'message' => 'id_teknisi diperlukan' + ], 400); + } + + $absensis = Absensi::where('id_teknisi', $id_teknisi) + ->whereMonth('tanggal', $bulan) + ->whereYear('tanggal', $tahun) + ->get(); + + $hadir = $absensis->where('status', 'hadir')->count(); + $izin = $absensis->where('status', 'izin')->count(); + $sakit = $absensis->where('status', 'sakit')->count(); + $total = $absensis->count(); + + // Hitung persentase kehadiran + $persentase = $total > 0 ? round(($hadir / $total) * 100, 1) : 0; + + // Hitung rata-rata jam masuk + $hadirItems = $absensis->where('status', 'hadir') + ->filter(fn($a) => $a->jam_masuk !== null); + + $rataJamMasuk = '-'; + $rataJamKeluar = '-'; + $rataDurasi = '-'; + $terlambat = 0; + $streak = 0; + + if ($hadirItems->count() > 0) { + // Rata-rata masuk + $totalMasukMenit = $hadirItems->sum(function ($a) { + return Carbon::parse($a->jam_masuk) + ->setTimezone('Asia/Jakarta') + ->hour * 60 + + Carbon::parse($a->jam_masuk) + ->setTimezone('Asia/Jakarta') + ->minute; + }); + $avgMasuk = round($totalMasukMenit / $hadirItems->count()); + $rataJamMasuk = sprintf('%02d:%02d', intdiv($avgMasuk, 60), $avgMasuk % 60); + + // Rata-rata keluar + $keluarItems = $hadirItems->filter(fn($a) => $a->jam_keluar !== null); + if ($keluarItems->count() > 0) { + $totalKeluarMenit = $keluarItems->sum(function ($a) { + return Carbon::parse($a->jam_keluar) + ->setTimezone('Asia/Jakarta') + ->hour * 60 + + Carbon::parse($a->jam_keluar) + ->setTimezone('Asia/Jakarta') + ->minute; + }); + $avgKeluar = round($totalKeluarMenit / $keluarItems->count()); + $rataJamKeluar = sprintf('%02d:%02d', intdiv($avgKeluar, 60), $avgKeluar % 60); + + // Rata-rata durasi + $totalDurasiMenit = $keluarItems->sum(fn($a) => $a->durasi_kerja); + $avgDurasi = round($totalDurasiMenit / $keluarItems->count()); + $jam = intdiv($avgDurasi, 60); + $menit = $avgDurasi % 60; + $rataDurasi = "{$jam}j {$menit}m"; + } + + // Hitung keterlambatan + $jadwalMasuk = '08:00'; + $terlambat = $hadirItems->filter(function ($a) use ($jadwalMasuk) { + $jamMasuk = Carbon::parse($a->jam_masuk)->setTimezone('Asia/Jakarta'); + $jamJadwal = Carbon::parse( + $a->tanggal->format('Y-m-d') . ' ' . $jadwalMasuk + )->setTimezone('Asia/Jakarta'); + return $jamMasuk->gt($jamJadwal); + })->count(); + } + + // Hitung streak (berturut-turut hadir dari hari ini mundur) + $sortedDesc = $absensis->sortByDesc('tanggal'); + foreach ($sortedDesc as $a) { + if ($a->status === 'hadir') $streak++; + else break; + } + + // Nama bulan Indonesia + $namaBulan = [ + 1=>'Januari',2=>'Februari',3=>'Maret',4=>'April', + 5=>'Mei',6=>'Juni',7=>'Juli',8=>'Agustus', + 9=>'September',10=>'Oktober',11=>'November',12=>'Desember' + ]; + + return response()->json([ + 'success' => true, + 'message' => 'Rekap absensi berhasil diambil', + 'data' => [ + 'bulan' => ($namaBulan[$bulan] ?? $bulan) . ' ' . $tahun, + 'total_hari_kerja'=> $total, + 'hadir' => $hadir, + 'izin' => $izin, + 'sakit' => $sakit, + 'persentase' => $persentase, + 'rata_masuk' => $rataJamMasuk, + 'rata_keluar' => $rataJamKeluar, + 'rata_durasi' => $rataDurasi, + 'keterlambatan' => $terlambat, + 'streak' => $streak, + ] + ], 200); + + } catch (\Exception $e) { + Log::error('Error in rekap: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil rekap absensi', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Mendapatkan status absensi per tanggal dalam 1 bulan (untuk kalender). + * Response: { "1": "hadir", "5": "izin", "10": "alpha", ... } + */ + public function kalender(Request $request) + { + try { + $id_teknisi = $request->query('id_teknisi'); + $bulan = $request->query('bulan', date('n')); + $tahun = $request->query('tahun', date('Y')); + + if (!$id_teknisi) { + return response()->json([ + 'success' => false, + 'message' => 'id_teknisi diperlukan' + ], 400); + } + + $absensis = Absensi::where('id_teknisi', $id_teknisi) + ->whereMonth('tanggal', $bulan) + ->whereYear('tanggal', $tahun) + ->get(['tanggal', 'status']); + + // Map tanggal (angka) => status + $data = []; + foreach ($absensis as $absensi) { + $tgl = (int) Carbon::parse($absensi->tanggal)->format('j'); + $data[$tgl] = $absensi->status; + } + + return response()->json([ + 'success' => true, + 'message' => 'Data kalender berhasil diambil', + 'data' => $data + ], 200); + + } catch (\Exception $e) { + Log::error('Error in kalender: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil data kalender', + 'error' => $e->getMessage() + ], 500); + } + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Http/Controllers/Api/AkunTeknisiApiController.php b/samooapk/laravel/app/Http/Controllers/Api/AkunTeknisiApiController.php new file mode 100644 index 0000000..1a7bdd9 --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/Api/AkunTeknisiApiController.php @@ -0,0 +1,44 @@ +belongsTo(Teknisi::class, 'id_teknisi', 'id_teknisi'); + } + + // JWT Methods + public function getJWTIdentifier() + { + return $this->getKey(); + } + + public function getJWTCustomClaims() + { + return []; + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Http/Controllers/Api/DashboardApiController.php b/samooapk/laravel/app/Http/Controllers/Api/DashboardApiController.php new file mode 100644 index 0000000..f1ad04c --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/Api/DashboardApiController.php @@ -0,0 +1,102 @@ +input('id_teknisi'); + + if (!$idTeknisi) { + return response()->json([ + 'success' => false, + 'message' => 'ID Teknisi tidak ditemukan' + ], 401); + } + + $teknisi = Teknisi::findOrFail($idTeknisi); + + // 1. Tugas Hari Ini / Aktif + $tugasAktif = Penugasan::where(function ($q) use ($idTeknisi) { + $q->where('id_teknisi', $idTeknisi) + ->orWhereHas('timTeknisi', function ($sq) use ($idTeknisi) { + $sq->where('id_teknisi', $idTeknisi); + }); + }) + ->whereIn('status_pekerjaan', ['belum_mulai', 'dalam_proses']) + ->count(); + + // 2. Gaji Bulan Berjalan (Estimasi Ongkos Kerja) + $now = Carbon::now(); + $estimasiGaji = Penugasan::where(function ($q) use ($idTeknisi) { + $q->where('id_teknisi', $idTeknisi) + ->orWhereHas('timTeknisi', function ($sq) use ($idTeknisi) { + $sq->where('id_teknisi', $idTeknisi); + }); + }) + ->where('status_pekerjaan', 'selesai') + ->whereMonth('tanggal_diselesaikan', $now->month) + ->whereYear('tanggal_diselesaikan', $now->year) + ->get() + ->sum(function($t) use ($idTeknisi) { + // Jika tim, bagi ongkos + if ($t->id_teknisi != $idTeknisi) { + $jumlahTim = $t->timTeknisi->count(); + return $jumlahTim > 0 ? $t->total_nilai_pekerjaan / $jumlahTim : 0; + } + return $t->total_nilai_pekerjaan; + }); + + // 3. Total Kasbon Aktif + $totalKasbon = Kasbon::where('id_teknisi', $idTeknisi) + ->where('status', 'belum_lunas') + ->sum('jumlah_kasbon'); + + // 4. Gaji Terakhir Diterima + $gajiTerakhir = Penggajian::where('id_teknisi', $idTeknisi) + ->where('status_pembayaran', 'sudah_bayar') + ->orderBy('periode_tahun', 'desc') + ->orderBy('periode_bulan', 'desc') + ->first(); + + return response()->json([ + 'success' => true, + 'message' => 'Data dashboard berhasil diambil', + 'data' => [ + 'teknisi' => [ + 'nama' => $teknisi->nama, + 'spesialisasi' => $teknisi->spesialisasi, + 'foto' => $teknisi->foto_url + ], + 'statistik' => [ + 'tugas_aktif' => $tugasAktif, + 'estimasi_gaji' => (float) $estimasiGaji, + 'total_kasbon' => (float) $totalKasbon, + 'gaji_terakhir' => $gajiTerakhir ? (float) $gajiTerakhir->gaji_bersih : 0, + 'periode_terakhir' => $gajiTerakhir ? Penggajian::getNamaBulan($gajiTerakhir->periode_bulan) . ' ' . $gajiTerakhir->periode_tahun : '-' + ] + ] + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil data dashboard: ' . $e->getMessage() + ], 500); + } + } +} diff --git a/samooapk/laravel/app/Http/Controllers/Api/GajiApiController.php b/samooapk/laravel/app/Http/Controllers/Api/GajiApiController.php new file mode 100644 index 0000000..4430928 --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/Api/GajiApiController.php @@ -0,0 +1,116 @@ +input('id_teknisi'); + + if (!$idTeknisi) { + return response()->json([ + 'success' => false, + 'message' => 'ID Teknisi tidak ditemukan' + ], 401); + } + + $riwayat = Penggajian::where('id_teknisi', $idTeknisi) + ->orderBy('periode_tahun', 'desc') + ->orderBy('periode_bulan', 'desc') + ->paginate(12); + + $riwayat->getCollection()->transform(function ($item) { + return [ + 'id_penggajian' => $item->id_penggajian, + 'periode_bulan' => $item->periode_bulan, + 'periode_tahun' => $item->periode_tahun, + 'nama_bulan' => Penggajian::getNamaBulan($item->periode_bulan), + 'tanggal_penggajian' => $item->tanggal_penggajian->format('d M Y'), + 'total_ongkos' => (float) $item->total_ongkos_pekerjaan, + 'potongan_kasbon' => (float) $item->total_kasbon, + 'potongan_makan' => (float) $item->biaya_makan, + 'gaji_bersih' => (float) $item->gaji_bersih, + 'status_pembayaran' => $item->status_pembayaran, + 'is_paid' => $item->isPaid(), + ]; + }); + + return response()->json([ + 'success' => true, + 'message' => 'Riwayat gaji berhasil diambil', + 'data' => $riwayat + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil data: ' . $e->getMessage() + ], 500); + } + } + + /** + * GET - Detail slip gaji + */ + public function show(Request $request, $id) + { + try { + $idTeknisi = $request->input('id_teknisi'); + $penggajian = Penggajian::with(['detailPenggajian.penugasan'])->findOrFail($id); + + // Keamanan: Pastikan teknisi hanya bisa lihat gajinya sendiri + if ($idTeknisi && $penggajian->id_teknisi != $idTeknisi) { + return response()->json([ + 'success' => false, + 'message' => 'Akses ditolak' + ], 403); + } + + $details = $penggajian->detailPenggajian->map(function ($detail) { + return [ + 'id_penugasan' => $detail->id_penugasan, + 'pekerjaan' => $detail->penugasan->label_jenis_pekerjaan ?? 'Pekerjaan', + 'lokasi' => $detail->lokasi, + 'bagian_ongkos' => (float) $detail->bagian_ongkos, + 'rincian' => $detail->rincian_pekerjaan, + 'tanggal_selesai' => $detail->tanggal_selesai ? $detail->tanggal_selesai->format('d/m/Y') : '-', + ]; + }); + + return response()->json([ + 'success' => true, + 'message' => 'Detail gaji berhasil diambil', + 'data' => [ + 'header' => [ + 'id_penggajian' => $penggajian->id_penggajian, + 'periode' => Penggajian::getNamaBulan($penggajian->periode_bulan) . ' ' . $penggajian->periode_tahun, + 'tanggal_hitung' => $penggajian->tanggal_penggajian->format('d M Y'), + 'hari_kerja' => $penggajian->jumlah_hari_kerja, + 'gaji_kotor' => (float) $penggajian->total_ongkos_pekerjaan, + 'potongan_kasbon' => (float) $penggajian->total_kasbon, + 'potongan_makan' => (float) $penggajian->biaya_makan, + 'gaji_bersih' => (float) $penggajian->gaji_bersih, + 'status_pembayaran' => $penggajian->status_pembayaran, + ], + 'items' => $details + ] + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Data tidak ditemukan: ' . $e->getMessage() + ], 404); + } + } +} diff --git a/samooapk/laravel/app/Http/Controllers/Api/KasbonApiController.php b/samooapk/laravel/app/Http/Controllers/Api/KasbonApiController.php new file mode 100644 index 0000000..48fe3b6 --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/Api/KasbonApiController.php @@ -0,0 +1,96 @@ +input('id_teknisi'); + + if (!$idTeknisi) { + return response()->json([ + 'success' => false, + 'message' => 'ID Teknisi tidak ditemukan' + ], 401); + } + + $query = Kasbon::where('id_teknisi', $idTeknisi) + ->orderBy('tanggal_kasbon', 'desc'); + + if ($request->filled('status')) { + $query->where('status', $request->status); + } + + $riwayat = $query->paginate(15); + + $riwayat->getCollection()->transform(function ($item) { + return [ + 'id_kasbon' => $item->id_kasbon, + 'nominal' => (float) $item->jumlah_kasbon, + 'tanggal' => $item->tanggal_kasbon->format('d M Y'), + 'keperluan' => $item->keperluan, + 'detail' => $item->keterangan_detail, + 'status' => $item->status, + 'status_label' => $item->status == 'lunas' ? 'Lunas' : 'Belum Lunas', + ]; + }); + + return response()->json([ + 'success' => true, + 'message' => 'Riwayat kasbon berhasil diambil', + 'data' => $riwayat + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil data: ' . $e->getMessage() + ], 500); + } + } + + /** + * GET - Statistik kasbon (total hutang) + */ + public function statistik(Request $request) + { + try { + $idTeknisi = $request->input('id_teknisi'); + + if (!$idTeknisi) { + return response()->json([ + 'success' => false, + 'message' => 'ID Teknisi tidak ditemukan' + ], 401); + } + + $totalHutang = Kasbon::where('id_teknisi', $idTeknisi) + ->where('status', 'belum_lunas') + ->sum('jumlah_kasbon'); + + return response()->json([ + 'success' => true, + 'message' => 'Statistik kasbon berhasil diambil', + 'data' => [ + 'total_hutang' => (float) $totalHutang + ] + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil statistik: ' . $e->getMessage() + ], 500); + } + } +} diff --git a/samooapk/laravel/app/Http/Controllers/Api/PenugasanApiController.php b/samooapk/laravel/app/Http/Controllers/Api/PenugasanApiController.php new file mode 100644 index 0000000..be880f9 --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/Api/PenugasanApiController.php @@ -0,0 +1,799 @@ +input('id_teknisi'); + + if (!$idTeknisi) { + return response()->json([ + 'success' => false, + 'message' => 'ID Teknisi tidak ditemukan' + ], 401); + } + + $query = Penugasan::with(['teknisi', 'tarif', 'timTeknisi.teknisi', 'items.tarif']) + ->where(function ($q) use ($idTeknisi) { + $q->where('id_teknisi', $idTeknisi) + ->orWhereHas('timTeknisi', function ($sq) use ($idTeknisi) { + $sq->where('id_teknisi', $idTeknisi); + }); + }); + + if ($request->filled('status')) { + $query->where('status_pekerjaan', $request->status); + } + + if ($request->filled('jenis_pekerjaan')) { + $query->where('jenis_pekerjaan', $request->jenis_pekerjaan); + } + + if ($request->filled('tanggal_mulai')) { + $query->whereDate('tanggal_diberikan', '>=', $request->tanggal_mulai); + } + + if ($request->filled('tanggal_akhir')) { + $query->whereDate('tanggal_diberikan', '<=', $request->tanggal_akhir); + } + + $penugasan = $query->orderBy('tanggal_diberikan', 'desc')->paginate(15); + + $penugasan->getCollection()->transform(function ($item) { + $namaTim = $item->timTeknisi->map(function ($tt) { + return $tt->teknisi->nama ?? 'N/A'; + })->implode(', '); + + $item->nama_tim = !empty($namaTim) ? $namaTim : ($item->teknisi->nama ?? 'N/A'); + + if ($item->teknisi) { + $item->teknisi->nama = $item->nama_tim; + } + + $item->foto_surat_url = $item->foto_surat_url; + $item->foto_sebelum_url = $item->foto_sebelum_url; + $item->foto_sesudah_url = $item->foto_sesudah_url; + $item->label_jenis_pekerjaan = $item->label_jenis_pekerjaan; + return $item; + }); + + return response()->json([ + 'success' => true, + 'message' => 'Data penugasan berhasil diambil', + 'data' => $penugasan + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil data: ' . $e->getMessage() + ], 500); + } + } + + /** + * GET - Detail penugasan + */ + public function show($id) + { + try { + $penugasan = Penugasan::with(['teknisi', 'tarif', 'timTeknisi.teknisi', 'items.tarif']) + ->findOrFail($id); + + $teamMembers = $penugasan->timTeknisi->map(function ($tt) { + return $tt->teknisi->nama ?? null; + })->filter()->unique()->values(); + + $namaTim = $teamMembers->implode(', '); + + $data = $penugasan->toArray(); + + $fullTeamNames = !empty($namaTim) ? $namaTim : ($penugasan->teknisi->nama ?? 'N/A'); + + if (isset($data['teknisi'])) { + $data['teknisi']['nama'] = $fullTeamNames; + } + + $prefix = !empty($namaTim) ? "[Tim: $namaTim] " : ""; + $data['catatan_admin'] = $prefix . ($penugasan->catatan_admin ?? ''); + $data['instruksi_tambahan'] = $data['catatan_admin']; + + $data['foto_surat_url'] = $penugasan->foto_surat_url; + $data['foto_sebelum_url'] = $penugasan->foto_sebelum_url; + $data['foto_sesudah_url'] = $penugasan->foto_sesudah_url; + $data['label_jenis_pekerjaan'] = $penugasan->label_jenis_pekerjaan; + $data['is_garansi_aktif'] = $penugasan->isGaransiAktif(); + $data['sisa_hari_garansi'] = $penugasan->getSisaHariGaransi(); + + return response()->json([ + 'success' => true, + 'message' => 'Detail penugasan berhasil diambil', + 'data' => $data + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Data tidak ditemukan: ' . $e->getMessage() + ], 404); + } + } + + /** + * POST - Teknisi melengkapi detail pekerjaan via mobile (pertama kali) + */ + public function lengkapiDetail(Request $request, $id) + { + try { + $penugasan = Penugasan::findOrFail($id); + + $validator = Validator::make($request->all(), [ + 'items' => 'required|array|min:1', + 'items.*.jenis_pekerjaan' => 'required|string', + 'items.*.dimensi_pipa' => 'nullable', + 'items.*.jarak_meter' => 'nullable|numeric', + 'items.*.jumlah_unit' => 'nullable|integer', + 'items.*.jumlah_titik' => 'nullable|integer', + 'items.*.pakai_pipa_besi' => 'nullable', + 'items.*.jenis_pengangkatan' => 'nullable', + 'detail_pekerjaan' => 'nullable|string', + 'tanggal_mulai' => 'required|date', + 'tim_teknisi' => 'nullable|array', + 'foto_sebelum' => 'nullable|file|max:10240', + 'foto_sesudah' => 'nullable|file|max:10240', + 'foto_sebelum_base64' => 'nullable|string', + 'foto_sesudah_base64' => 'nullable|string', + ]); + + if ($validator->fails()) { + \Illuminate\Support\Facades\Log::error('Validation Fail in lengkapiDetail', $validator->errors()->toArray()); + return response()->json([ + 'success' => false, + 'message' => 'Validasi gagal', + 'errors' => $validator->errors() + ], 422); + } + + DB::beginTransaction(); + + \App\Models\PenugasanItem::where('id_penugasan', $penugasan->id_penugasan)->delete(); + + $fotoSebelum = $penugasan->foto_sebelum; + if ($request->hasFile('foto_sebelum')) { + $fotoSebelum = $request->file('foto_sebelum')->store('penugasan/foto-sebelum', 'public'); + } elseif ($request->foto_sebelum_base64) { + $fotoSebelum = $this->storeBase64($request->foto_sebelum_base64, 'penugasan/foto-sebelum'); + } + + $fotoSesudah = $penugasan->foto_sesudah; + if ($request->hasFile('foto_sesudah')) { + $fotoSesudah = $request->file('foto_sesudah')->store('penugasan/foto-sesudah', 'public'); + } elseif ($request->foto_sesudah_base64) { + $fotoSesudah = $this->storeBase64($request->foto_sesudah_base64, 'penugasan/foto-sesudah'); + } + + $totalNilaiPenugasan = 0; + $hasSR = false; + + foreach ($request->items as $itemData) { + $tarif = TarifPekerjaan::where('jenis_pekerjaan', $itemData['jenis_pekerjaan']) + ->where('is_active', true); + + if (!empty($itemData['dimensi_pipa'])) { + $tarif->where('dimensi_pipa', $itemData['dimensi_pipa']); + } + + if (isset($itemData['pakai_pipa_besi'])) { + $tarif->where('pakai_pipa_besi', $itemData['pakai_pipa_besi']); + } + + $tarif = $tarif->first(); + $nilaiItem = $this->hitungNilaiItem($tarif, $itemData); + $totalNilaiPenugasan += $nilaiItem; + + \App\Models\PenugasanItem::create([ + 'id_penugasan' => $penugasan->id_penugasan, + 'id_tarif' => $tarif ? $tarif->id_tarif : null, + 'jenis_pekerjaan' => $itemData['jenis_pekerjaan'], + 'dimensi_pipa' => $itemData['dimensi_pipa'] ?? null, + 'jarak_meter' => $itemData['jarak_meter'] ?? null, + 'jumlah_unit' => $itemData['jumlah_unit'] ?? null, + 'jumlah_titik' => $itemData['jumlah_titik'] ?? null, + 'pakai_pipa_besi' => $itemData['pakai_pipa_besi'] ?? null, + 'jenis_pengangkatan' => $itemData['jenis_pengangkatan'] ?? null, + 'total_nilai_pekerjaan' => $nilaiItem, + ]); + + if ($itemData['jenis_pekerjaan'] === 'sr') $hasSR = true; + } + + $firstItem = $request->items[0]; + + $penugasan->update([ + 'jenis_pekerjaan' => $firstItem['jenis_pekerjaan'], + 'dimensi_pipa' => $firstItem['dimensi_pipa'] ?? $penugasan->dimensi_pipa, + 'jarak_meter' => $firstItem['jarak_meter'] ?? $penugasan->jarak_meter, + 'jumlah_unit' => $firstItem['jumlah_unit'] ?? $penugasan->jumlah_unit, + 'pakai_pipa_besi' => array_key_exists('pakai_pipa_besi', $firstItem) + ? $firstItem['pakai_pipa_besi'] + : $penugasan->pakai_pipa_besi, + 'status_pekerjaan' => $penugasan->status_pekerjaan === 'belum_mulai' + ? 'dalam_proses' + : $penugasan->status_pekerjaan, + 'total_nilai_pekerjaan' => $totalNilaiPenugasan, + 'detail_pekerjaan' => $request->has('detail_pekerjaan') + ? $request->detail_pekerjaan + : $penugasan->detail_pekerjaan, + 'tanggal_mulai' => $request->tanggal_mulai, + 'foto_sebelum' => $fotoSebelum, + 'foto_sesudah' => $fotoSesudah, + ]); + + if ($hasSR) { + $penugasan->setGaransiMeteranAir($request->tanggal_mulai); + $penugasan->save(); + } + + if ($request->filled('tim_teknisi')) { + foreach ($request->tim_teknisi as $idTeknisiTambahan) { + $exists = TimTeknisiPenugasan::where('id_penugasan', $penugasan->id_penugasan) + ->where('id_teknisi', $idTeknisiTambahan) + ->exists(); + + if (!$exists) { + TimTeknisiPenugasan::create([ + 'id_penugasan' => $penugasan->id_penugasan, + 'id_teknisi' => $idTeknisiTambahan, + 'status_kehadiran' => 'hadir', + ]); + } + } + } + + DB::commit(); + + $penugasan->load(['teknisi', 'tarif', 'timTeknisi.teknisi', 'items.tarif']); + + return response()->json([ + 'success' => true, + 'message' => 'Detail pekerjaan berhasil dilengkapi!', + 'data' => $penugasan + ]); + + } catch (Exception $e) { + DB::rollBack(); + return response()->json([ + 'success' => false, + 'message' => 'Gagal melengkapi detail: ' . $e->getMessage() + ], 500); + } + } + + /** + * PUT - Update / edit detail pekerjaan yang sudah diisi sebelumnya (teknisi via mobile) + */ + public function updateDetail(Request $request, $id) + { + try { + $penugasan = Penugasan::with(['timTeknisi'])->findOrFail($id); + + $validator = Validator::make($request->all(), [ + 'id_teknisi' => 'required|integer', + 'items' => 'required|array|min:1', + 'items.*.id_penugasan_item' => 'nullable|integer', + 'items.*.jenis_pekerjaan' => 'required|string', + 'items.*.dimensi_pipa' => 'nullable', + 'items.*.jarak_meter' => 'nullable|numeric', + 'items.*.jumlah_unit' => 'nullable|integer', + 'items.*.jumlah_titik' => 'nullable|integer', + 'items.*.pakai_pipa_besi' => 'nullable', + 'items.*.jenis_pengangkatan' => 'nullable', + 'detail_pekerjaan' => 'nullable|string', + 'tanggal_mulai' => 'nullable|date', + 'tim_teknisi' => 'nullable|array', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validasi gagal', + 'errors' => $validator->errors() + ], 422); + } + + $idTeknisiEditor = $request->id_teknisi; + $isAssigned = ($penugasan->id_teknisi == $idTeknisiEditor) || + $penugasan->timTeknisi->pluck('id_teknisi')->contains($idTeknisiEditor); + + if (!$isAssigned) { + return response()->json([ + 'success' => false, + 'message' => 'Anda tidak berwenang mengedit penugasan ini' + ], 403); + } + + DB::beginTransaction(); + + \App\Models\PenugasanItem::where('id_penugasan', $penugasan->id_penugasan)->delete(); + + $hasSR = false; + $processedItemIds = []; + + foreach ($request->items as $itemData) { + $tarifQuery = TarifPekerjaan::where('jenis_pekerjaan', $itemData['jenis_pekerjaan']) + ->where('is_active', true); + + if (!empty($itemData['dimensi_pipa'])) { + $tarifQuery->where('dimensi_pipa', $itemData['dimensi_pipa']); + } + + if (isset($itemData['pakai_pipa_besi'])) { + $tarifQuery->where('pakai_pipa_besi', $itemData['pakai_pipa_besi']); + } + + $tarif = $tarifQuery->first(); + $nilaiItem = $this->hitungNilaiItem($tarif, $itemData); + + $created = \App\Models\PenugasanItem::create([ + 'id_penugasan' => $penugasan->id_penugasan, + 'id_tarif' => $tarif ? $tarif->id_tarif : null, + 'jenis_pekerjaan' => $itemData['jenis_pekerjaan'], + 'dimensi_pipa' => $itemData['dimensi_pipa'] ?? null, + 'jarak_meter' => $itemData['jarak_meter'] ?? null, + 'jumlah_unit' => $itemData['jumlah_unit'] ?? null, + 'jumlah_titik' => $itemData['jumlah_titik'] ?? null, + 'pakai_pipa_besi' => $itemData['pakai_pipa_besi'] ?? null, + 'jenis_pengangkatan' => $itemData['jenis_pengangkatan'] ?? null, + 'total_nilai_pekerjaan' => $nilaiItem, + ]); + $processedItemIds[] = $created->id_penugasan_item; + + if ($itemData['jenis_pekerjaan'] === 'sr') $hasSR = true; + } + + if ($request->has('detail_pekerjaan')) { + $penugasan->detail_pekerjaan = $request->detail_pekerjaan; + } + + if ($request->filled('tanggal_mulai')) { + $penugasan->tanggal_mulai = $request->tanggal_mulai; + } + + if ($request->filled('tim_teknisi')) { + foreach ($request->tim_teknisi as $idTeknisiTambahan) { + $exists = TimTeknisiPenugasan::where('id_penugasan', $penugasan->id_penugasan) + ->where('id_teknisi', $idTeknisiTambahan) + ->exists(); + + if (!$exists) { + TimTeknisiPenugasan::create([ + 'id_penugasan' => $penugasan->id_penugasan, + 'id_teknisi' => $idTeknisiTambahan, + 'status_kehadiran' => 'hadir', + ]); + } + } + } + + $total = \App\Models\PenugasanItem::where('id_penugasan', $penugasan->id_penugasan) + ->sum('total_nilai_pekerjaan'); + + $firstItem = $request->items[0]; + + $penugasan->total_nilai_pekerjaan = $total; + $penugasan->jenis_pekerjaan = $firstItem['jenis_pekerjaan'] ?? $penugasan->jenis_pekerjaan; + $penugasan->dimensi_pipa = $firstItem['dimensi_pipa'] ?? $penugasan->dimensi_pipa; + $penugasan->jarak_meter = $firstItem['jarak_meter'] ?? $penugasan->jarak_meter; + $penugasan->jumlah_unit = $firstItem['jumlah_unit'] ?? $penugasan->jumlah_unit; + $penugasan->pakai_pipa_besi = array_key_exists('pakai_pipa_besi', $firstItem) + ? $firstItem['pakai_pipa_besi'] + : $penugasan->pakai_pipa_besi; + + $statusBolehDiubah = ['belum_mulai']; + if (in_array($penugasan->status_pekerjaan, $statusBolehDiubah)) { + $penugasan->status_pekerjaan = 'dalam_proses'; + } + + $penugasan->save(); + + if ($hasSR) { + $penugasan->setGaransiMeteranAir($penugasan->tanggal_mulai ?? null); + $penugasan->save(); + } + + DB::commit(); + + $penugasan->load(['teknisi', 'tarif', 'timTeknisi.teknisi', 'items.tarif']); + + return response()->json([ + 'success' => true, + 'message' => 'Detail pekerjaan berhasil diperbarui!', + 'data' => $penugasan + ]); + + } catch (Exception $e) { + DB::rollBack(); + return response()->json([ + 'success' => false, + 'message' => 'Gagal memperbarui detail: ' . $e->getMessage() + ], 500); + } + } + + /** + * POST - Tambah rincian pekerjaan baru di tengah progres + */ + public function addItem(Request $request, $id) + { + try { + $penugasan = Penugasan::findOrFail($id); + + $validator = Validator::make($request->all(), [ + 'jenis_pekerjaan' => 'required|string', + 'dimensi_pipa' => 'nullable', + 'jarak_meter' => 'nullable|numeric', + 'jumlah_unit' => 'nullable|integer', + 'jumlah_titik' => 'nullable|integer', + 'pakai_pipa_besi' => 'nullable', + 'jenis_pengangkatan' => 'nullable', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validasi gagal', + 'errors' => $validator->errors() + ], 422); + } + + DB::beginTransaction(); + + $tarif = TarifPekerjaan::where('jenis_pekerjaan', $request->jenis_pekerjaan) + ->where('is_active', true); + + if ($request->filled('dimensi_pipa')) { + $tarif->where('dimensi_pipa', $request->dimensi_pipa); + } + + if ($request->has('pakai_pipa_besi')) { + $tarif->where('pakai_pipa_besi', $request->pakai_pipa_besi); + } + + $tarif = $tarif->first(); + $nilaiItem = $this->hitungNilaiItem($tarif, $request->all()); + + \App\Models\PenugasanItem::create([ + 'id_penugasan' => $penugasan->id_penugasan, + 'id_tarif' => $tarif ? $tarif->id_tarif : null, + 'jenis_pekerjaan' => $request->jenis_pekerjaan, + 'dimensi_pipa' => $request->dimensi_pipa, + 'jarak_meter' => $request->jarak_meter, + 'jumlah_unit' => $request->jumlah_unit, + 'jumlah_titik' => $request->jumlah_titik, + 'pakai_pipa_besi' => $request->pakai_pipa_besi, + 'jenis_pengangkatan' => $request->jenis_pengangkatan, + 'total_nilai_pekerjaan' => $nilaiItem, + ]); + + $total = \App\Models\PenugasanItem::where('id_penugasan', $penugasan->id_penugasan) + ->sum('total_nilai_pekerjaan'); + $penugasan->total_nilai_pekerjaan = $total; + $penugasan->save(); + + DB::commit(); + + return response()->json([ + 'success' => true, + 'message' => 'Rincian pekerjaan berhasil ditambahkan!', + 'data' => [ + 'nilai_item' => $nilaiItem, + 'total_baru' => $penugasan->total_nilai_pekerjaan + ] + ]); + + } catch (Exception $e) { + DB::rollBack(); + return response()->json([ + 'success' => false, + 'message' => 'Gagal menambah rincian: ' . $e->getMessage() + ], 500); + } + } + + /** + * PUT - Update status pekerjaan + * + * ✅ FIX: Ketika status diubah menjadi 'selesai', semua anggota tim + * otomatis dicatat status_kehadiran = 'hadir' di tabel tim_teknisi_penugasans. + * Ini memastikan semua anggota tim terhitung gajinya meskipun + * bukan dia yang menerima/menceklis tugas di awal. + */ + public function updateStatus(Request $request, $id) + { + try { + $penugasan = Penugasan::findOrFail($id); + + $validator = Validator::make($request->all(), [ + 'status_pekerjaan' => 'required|in:belum_mulai,dalam_proses,selesai,dibatalkan', + 'tanggal_diselesaikan' => 'required_if:status_pekerjaan,selesai|nullable|date', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validasi gagal', + 'errors' => $validator->errors() + ], 422); + } + + DB::beginTransaction(); + + $updateData = ['status_pekerjaan' => $request->status_pekerjaan]; + + if ($request->status_pekerjaan === 'selesai') { + $updateData['tanggal_diselesaikan'] = $request->tanggal_diselesaikan ?? now(); + + // ✅ FIX: Pastikan semua anggota tim tercatat hadir + // sehingga gaji semua anggota tim terhitung dengan benar + TimTeknisiPenugasan::where('id_penugasan', $penugasan->id_penugasan) + ->update(['status_kehadiran' => 'hadir']); + } + + $penugasan->update($updateData); + + DB::commit(); + + return response()->json([ + 'success' => true, + 'message' => 'Status pekerjaan berhasil diupdate!', + 'data' => $penugasan + ]); + + } catch (Exception $e) { + DB::rollBack(); + return response()->json([ + 'success' => false, + 'message' => 'Gagal update status: ' . $e->getMessage() + ], 500); + } + } + + /** + * POST - Upload foto sebelum/sesudah + */ + public function uploadFoto(Request $request, $id) + { + try { + $penugasan = Penugasan::findOrFail($id); + + \Illuminate\Support\Facades\Log::info('Upload Foto Request Received', [ + 'id_penugasan' => $id, + 'tipe_foto' => $request->tipe_foto, + 'has_file' => $request->hasFile('foto'), + ]); + + $validator = Validator::make($request->all(), [ + 'tipe_foto' => 'required|in:sebelum,sesudah', + 'foto' => 'nullable|image|mimes:jpeg,png,jpg|max:10240', + 'sebelum_base64' => 'nullable|string', + 'sesudah_base64' => 'nullable|string', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validasi gagal', + 'errors' => $validator->errors() + ], 422); + } + + $tipeFoto = $request->tipe_foto; + $fotoPath = null; + + if ($request->hasFile('foto')) { + $fotoPath = $request->file('foto')->store("penugasan/foto-{$tipeFoto}", 'public'); + } elseif ($request->input($tipeFoto . '_base64')) { + $fotoPath = $this->storeBase64($request->input($tipeFoto . '_base64'), "penugasan/foto-{$tipeFoto}"); + } + + if (!$fotoPath) { + return response()->json([ + 'success' => false, + 'message' => 'Tidak ada foto yang diupload' + ], 422); + } + + if ($tipeFoto === 'sebelum') { + $penugasan->foto_sebelum = $fotoPath; + } else { + $penugasan->foto_sesudah = $fotoPath; + } + $penugasan->save(); + + return response()->json([ + 'success' => true, + 'message' => "Foto {$tipeFoto} berhasil diupload!", + 'data' => [ + 'foto_url' => asset("storage/{$fotoPath}"), + 'foto_path' => $fotoPath + ] + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal upload foto: ' . $e->getMessage() + ], 500); + } + } + + /** + * GET - Tarif berdasarkan jenis pekerjaan + */ + public function getTarifByJenis(Request $request) + { + try { + $validator = Validator::make($request->all(), [ + 'jenis_pekerjaan' => 'required|in:sr,pengembangan_jaringan_pipa,pengangkatan,pemasangan_gate_valve,gali_urug,perbaikan_jaringan_pipa,pengecatan_pipa_besi,penyempurnaan_jaringan_pipa', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Jenis pekerjaan tidak valid', + 'errors' => $validator->errors() + ], 422); + } + + $tarifs = TarifPekerjaan::where('jenis_pekerjaan', $request->jenis_pekerjaan) + ->where('is_active', true) + ->get(); + + return response()->json([ + 'success' => true, + 'message' => 'Data tarif berhasil diambil', + 'data' => $tarifs + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil data tarif: ' . $e->getMessage() + ], 500); + } + } + + /** + * GET - Statistik penugasan teknisi + */ + public function statistik(Request $request) + { + try { + $idTeknisi = $request->input('id_teknisi'); + + if (!$idTeknisi) { + return response()->json([ + 'success' => false, + 'message' => 'ID Teknisi tidak ditemukan' + ], 401); + } + + $baseQuery = Penugasan::where(function ($q) use ($idTeknisi) { + $q->where('id_teknisi', $idTeknisi) + ->orWhereHas('timTeknisi', function ($sq) use ($idTeknisi) { + $sq->where('id_teknisi', $idTeknisi); + }); + }); + + $statistik = [ + 'total_penugasan' => (clone $baseQuery)->count(), + 'belum_mulai' => (clone $baseQuery)->where('status_pekerjaan', 'belum_mulai')->count(), + 'dalam_proses' => (clone $baseQuery)->where('status_pekerjaan', 'dalam_proses')->count(), + 'selesai' => (clone $baseQuery)->where('status_pekerjaan', 'selesai')->count(), + 'menunggu_detail' => (clone $baseQuery)->whereNull('jenis_pekerjaan')->count(), + 'detail_lengkap' => (clone $baseQuery)->whereNotNull('jenis_pekerjaan')->count(), + ]; + + return response()->json([ + 'success' => true, + 'message' => 'Statistik berhasil diambil', + 'data' => $statistik + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil statistik: ' . $e->getMessage() + ], 500); + } + } + + /** + * GET - Daftar teknisi aktif untuk tambah tim + */ + public function getTeknisiList() + { + try { + $teknisi = Teknisi::where('status', 'aktif') + ->orderBy('nama') + ->get(['id_teknisi', 'nama', 'no_telepon', 'spesialisasi']); + + return response()->json([ + 'success' => true, + 'message' => 'Daftar teknisi berhasil diambil', + 'data' => $teknisi + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil data teknisi: ' . $e->getMessage() + ], 500); + } + } + + // =================================== + // PRIVATE HELPER + // =================================== + + private function hitungNilaiItem($tarif, array $data): float + { + if (!$tarif) return 0; + + if ($tarif->tarif_per_meter && !empty($data['jarak_meter'])) { + return (float)$tarif->tarif_per_meter * (float)$data['jarak_meter']; + } + + if ($tarif->tarif_per_unit && !empty($data['jumlah_unit'])) { + return (float)$tarif->tarif_per_unit * (int)$data['jumlah_unit']; + } + + if ($tarif->tarif_per_unit && !empty($data['jumlah_titik'])) { + return (float)$tarif->tarif_per_unit * (int)$data['jumlah_titik']; + } + + return (float)($tarif->tarif_per_unit ?? 0); + } + + private function storeBase64($base64String, $folder) + { + try { + if (preg_match("/^data:image\/(\w+);base64,/", $base64String, $type)) { + $base64String = substr($base64String, strpos($base64String, ",") + 1); + $type = strtolower($type[1]); + } else { + $type = "jpg"; + } + + $image = base64_decode($base64String); + if ($image === false) return null; + + $fileName = \Illuminate\Support\Str::random(40) . "." . $type; + $path = $folder . "/" . $fileName; + + \Illuminate\Support\Facades\Storage::disk("public")->put($path, $image); + + return $path; + } catch (Exception $e) { + \Illuminate\Support\Facades\Log::error("Base64 Store Error: " . $e->getMessage()); + return null; + } + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/samooapk/laravel/app/Http/Controllers/Auth/AuthenticatedSessionController.php new file mode 100644 index 0000000..789a9b0 --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -0,0 +1,50 @@ +authenticate(); + + $request->session()->regenerate(); + + // Redirect ke dashboard kamu (bukan RouteServiceProvider::HOME) + return redirect()->intended(route('dashboard')); + } + + /** + * Destroy an authenticated session. + */ + public function destroy(Request $request): RedirectResponse + { + Auth::guard('web')->logout(); + + $request->session()->invalidate(); + + $request->session()->regenerateToken(); + + // Redirect ke halaman home/welcome setelah logout + return redirect('/'); + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Http/Controllers/Auth/ConfirmablePasswordController.php b/samooapk/laravel/app/Http/Controllers/Auth/ConfirmablePasswordController.php new file mode 100644 index 0000000..523ddda --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/Auth/ConfirmablePasswordController.php @@ -0,0 +1,41 @@ +validate([ + 'email' => $request->user()->email, + 'password' => $request->password, + ])) { + throw ValidationException::withMessages([ + 'password' => __('auth.password'), + ]); + } + + $request->session()->put('auth.password_confirmed_at', time()); + + return redirect()->intended(RouteServiceProvider::HOME); + } +} diff --git a/samooapk/laravel/app/Http/Controllers/Auth/EmailVerificationNotificationController.php b/samooapk/laravel/app/Http/Controllers/Auth/EmailVerificationNotificationController.php new file mode 100644 index 0000000..96ba772 --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/Auth/EmailVerificationNotificationController.php @@ -0,0 +1,25 @@ +user()->hasVerifiedEmail()) { + return redirect()->intended(RouteServiceProvider::HOME); + } + + $request->user()->sendEmailVerificationNotification(); + + return back()->with('status', 'verification-link-sent'); + } +} diff --git a/samooapk/laravel/app/Http/Controllers/Auth/EmailVerificationPromptController.php b/samooapk/laravel/app/Http/Controllers/Auth/EmailVerificationPromptController.php new file mode 100644 index 0000000..186eb97 --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/Auth/EmailVerificationPromptController.php @@ -0,0 +1,22 @@ +user()->hasVerifiedEmail() + ? redirect()->intended(RouteServiceProvider::HOME) + : view('auth.verify-email'); + } +} diff --git a/samooapk/laravel/app/Http/Controllers/Auth/NewPasswordController.php b/samooapk/laravel/app/Http/Controllers/Auth/NewPasswordController.php new file mode 100644 index 0000000..f1e2814 --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/Auth/NewPasswordController.php @@ -0,0 +1,61 @@ + $request]); + } + + /** + * Handle an incoming new password request. + * + * @throws \Illuminate\Validation\ValidationException + */ + public function store(Request $request): RedirectResponse + { + $request->validate([ + 'token' => ['required'], + 'email' => ['required', 'email'], + 'password' => ['required', 'confirmed', Rules\Password::defaults()], + ]); + + // Here we will attempt to reset the user's password. If it is successful we + // will update the password on an actual user model and persist it to the + // database. Otherwise we will parse the error and return the response. + $status = Password::reset( + $request->only('email', 'password', 'password_confirmation', 'token'), + function ($user) use ($request) { + $user->forceFill([ + 'password' => Hash::make($request->password), + 'remember_token' => Str::random(60), + ])->save(); + + event(new PasswordReset($user)); + } + ); + + // If the password was successfully reset, we will redirect the user back to + // the application's home authenticated view. If there is an error we can + // redirect them back to where they came from with their error message. + return $status == Password::PASSWORD_RESET + ? redirect()->route('login')->with('status', __($status)) + : back()->withInput($request->only('email')) + ->withErrors(['email' => __($status)]); + } +} diff --git a/samooapk/laravel/app/Http/Controllers/Auth/PasswordController.php b/samooapk/laravel/app/Http/Controllers/Auth/PasswordController.php new file mode 100644 index 0000000..6916409 --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/Auth/PasswordController.php @@ -0,0 +1,29 @@ +validateWithBag('updatePassword', [ + 'current_password' => ['required', 'current_password'], + 'password' => ['required', Password::defaults(), 'confirmed'], + ]); + + $request->user()->update([ + 'password' => Hash::make($validated['password']), + ]); + + return back()->with('status', 'password-updated'); + } +} diff --git a/samooapk/laravel/app/Http/Controllers/Auth/PasswordResetLinkController.php b/samooapk/laravel/app/Http/Controllers/Auth/PasswordResetLinkController.php new file mode 100644 index 0000000..ce813a6 --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/Auth/PasswordResetLinkController.php @@ -0,0 +1,44 @@ +validate([ + 'email' => ['required', 'email'], + ]); + + // We will send the password reset link to this user. Once we have attempted + // to send the link, we will examine the response then see the message we + // need to show to the user. Finally, we'll send out a proper response. + $status = Password::sendResetLink( + $request->only('email') + ); + + return $status == Password::RESET_LINK_SENT + ? back()->with('status', __($status)) + : back()->withInput($request->only('email')) + ->withErrors(['email' => __($status)]); + } +} diff --git a/samooapk/laravel/app/Http/Controllers/Auth/RegisteredUserController.php b/samooapk/laravel/app/Http/Controllers/Auth/RegisteredUserController.php new file mode 100644 index 0000000..a15828f --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/Auth/RegisteredUserController.php @@ -0,0 +1,51 @@ +validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], + 'password' => ['required', 'confirmed', Rules\Password::defaults()], + ]); + + $user = User::create([ + 'name' => $request->name, + 'email' => $request->email, + 'password' => Hash::make($request->password), + ]); + + event(new Registered($user)); + + Auth::login($user); + + return redirect(RouteServiceProvider::HOME); + } +} diff --git a/samooapk/laravel/app/Http/Controllers/Auth/VerifyEmailController.php b/samooapk/laravel/app/Http/Controllers/Auth/VerifyEmailController.php new file mode 100644 index 0000000..ea87940 --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/Auth/VerifyEmailController.php @@ -0,0 +1,28 @@ +user()->hasVerifiedEmail()) { + return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); + } + + if ($request->user()->markEmailAsVerified()) { + event(new Verified($request->user())); + } + + return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); + } +} diff --git a/samooapk/laravel/app/Http/Controllers/Controller.php b/samooapk/laravel/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..858c1b1 --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/Controller.php @@ -0,0 +1,13 @@ +count(); // sesuaikan nanti kalau error +$totalPekerjaan = Penugasan::count(); +$totalLaporan = 0; +$penugasanAktif = Penugasan::where('status_pekerjaan', 'dalam_proses')->count(); // ✅ + + // ── BAR CHART & DONUT ──────────────────────────────── + $chartData = PenugasanItem::select('jenis_pekerjaan', DB::raw('count(*) as total')) + ->groupBy('jenis_pekerjaan') + ->pluck('total', 'jenis_pekerjaan'); + + $jenisMapping = [ + 'sr' => 'Sambungan Rumah (SR)', + 'pengembangan_jaringan_pipa' => 'Pengembangan Pipa', + 'perbaikan_jaringan_pipa' => 'Perbaikan Pipa', + 'gali_urug' => 'Gali Urug', + 'pemasangan_gate_valve' => 'Pemas. Gate Valve', + 'pengangkatan' => 'Pengangkatan', + 'pengecatan_pipa_besi' => 'Pengecatan Pipa', + 'penyempurnaan_jaringan_pipa' => 'Penyempurnaan Pipa', + ]; + + $chartColors = ['#1d9e75', '#378add', '#e24b4a', '#ef9f27', '#7f77dd', '#a552cc', '#34d399', '#f472b6']; + + $chartLabels = []; + $chartValues = []; + $chartBgColors = []; + + $i = 0; + foreach($jenisMapping as $key => $label) { + $chartLabels[] = $label; + $chartValues[] = $chartData->get($key, 0); + $chartBgColors[] = $chartColors[$i % count($chartColors)]; + $i++; + } + + // ── LINE CHART TEKNISI (MULTI-LINE PER BULAN) ────────────────── + $months = []; + $monthLabels = []; + for ($i = 5; $i >= 0; $i--) { + $date = Carbon::now()->subMonths($i); + $months[] = $date->format('Y-m'); + $monthLabels[] = $date->translatedFormat('F'); // ex: Januari, Februari + } + + $teknisiData = Penugasan::join('teknisis', 'penugasans.id_teknisi', '=', 'teknisis.id_teknisi') + ->select('teknisis.nama', DB::raw('DATE_FORMAT(penugasans.created_at, "%Y-%m") as month_year'), DB::raw('count(penugasans.id_penugasan) as total')) + ->where('penugasans.created_at', '>=', Carbon::now()->subMonths(5)->startOfMonth()) + ->groupBy('teknisis.id_teknisi', 'teknisis.nama', 'month_year') + ->get(); + + $teknisiDatasets = []; + $lineColors = ['#378add', '#e24b4a', '#1d9e75', '#ef9f27', '#7f77dd', '#a552cc', '#f472b6', '#34d399']; + $colorIdx = 0; + + foreach ($teknisiData->groupBy('nama') as $nama => $records) { + $dataArray = []; + foreach ($months as $m) { + $record = $records->firstWhere('month_year', $m); + $dataArray[] = $record ? $record->total : 0; + } + + $teknisiDatasets[] = [ + 'label' => $nama, + 'data' => $dataArray, + 'borderColor' => $lineColors[$colorIdx % count($lineColors)], + 'backgroundColor' => 'transparent', + 'borderWidth' => 2, + 'pointBackgroundColor' => '#fff', + 'pointBorderColor' => $lineColors[$colorIdx % count($lineColors)], + 'pointBorderWidth' => 2, + 'pointRadius' => 4, + 'tension' => 0.3 + ]; + $colorIdx++; + } + + // ── TARIF PEKERJAAN ────────────────────────────────── + $tarifPekerjaans = TarifPekerjaan::where('is_active', true) + ->orderBy('jenis_pekerjaan') + ->orderBy('dimensi_pipa') + ->get(); + + // ── KONTRAK — tidak ada, kirim collection kosong ───── + $kontrakJatuhTempo = collect(); + + return view('dashboard', compact( + 'totalTeknisi', + 'teknisiAktif', + 'totalPekerjaan', + 'totalLaporan', + 'penugasanAktif', + 'chartLabels', + 'chartValues', + 'chartBgColors', + 'monthLabels', + 'teknisiDatasets', + 'tarifPekerjaans', + 'kontrakJatuhTempo', + )); + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Http/Controllers/KasbonController.php b/samooapk/laravel/app/Http/Controllers/KasbonController.php new file mode 100644 index 0000000..ddfbc95 --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/KasbonController.php @@ -0,0 +1,486 @@ +has('status') && $request->status != '') { + $query->byStatus($request->status); + } + + // Filter berdasarkan teknisi jika ada + if ($request->has('id_teknisi') && $request->id_teknisi != '') { + $query->where('id_teknisi', $request->id_teknisi); + } + + // Filter berdasarkan tanggal + if ($request->has('tanggal_dari') && $request->tanggal_dari != '') { + $query->whereDate('tanggal_kasbon', '>=', $request->tanggal_dari); + } + + if ($request->has('tanggal_sampai') && $request->tanggal_sampai != '') { + $query->whereDate('tanggal_kasbon', '<=', $request->tanggal_sampai); + } + + // Sorting + $sortBy = $request->get('sort_by', 'created_at'); + $sortOrder = $request->get('sort_order', 'desc'); + $query->orderBy($sortBy, $sortOrder); + + // Pagination + $perPage = $request->get('per_page', 15); + $kasbons = $query->paginate($perPage); + + // Statistik (Disederhanakan untuk efisiensi) + $totalKasbon = Kasbon::count(); + $totalNominal = Kasbon::sum('jumlah_kasbon'); + $kasbonLunas = Kasbon::where('status', 'lunas')->count(); + $kasbonBelumLunas = Kasbon::where('status', 'belum_lunas')->count(); + $totalNominalBelumLunas = Kasbon::where('status', 'belum_lunas')->sum('jumlah_kasbon'); + + // Daftar teknisi untuk dropdown modal & filter + $teknisis = Teknisi::orderBy('nama')->get(); + + // Untuk API response + if ($request->expectsJson()) { + return response()->json([ + 'success' => true, + 'data' => $kasbons, + 'message' => 'Data kasbon berhasil diambil' + ]); + } + + return view('Admin.Gaji.Kasbon', compact( + 'kasbons', + 'totalKasbon', + 'totalNominal', + 'kasbonLunas', + 'kasbonBelumLunas', + 'totalNominalBelumLunas', + 'teknisis' + )); + } + + /** + * Show the form for creating a new resource. + * + * @return View + */ + public function create(): View + { + $statusOptions = Kasbon::getStatusOptions(); + return view('Admin.Gaji.create-kasbon', compact('statusOptions')); // DIPERBAIKI: path view + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * @return RedirectResponse|JsonResponse + */ + public function store(Request $request) + { + \Illuminate\Support\Facades\Log::info('Kasbon Store Request Received', $request->all()); + $validator = Validator::make($request->all(), [ + 'id_teknisi' => 'required|integer|min:1', + 'jumlah_kasbon' => 'required|numeric|min:0', + 'tanggal_kasbon' => 'required|date', + 'status' => 'nullable|in:lunas,belum_lunas', + 'keterangan' => 'nullable|string|max:100' + ], [ + 'id_teknisi.required' => 'ID Teknisi harus diisi', + 'id_teknisi.integer' => 'ID Teknisi harus berupa angka', + 'jumlah_kasbon.required' => 'Jumlah kasbon harus diisi', + 'jumlah_kasbon.numeric' => 'Jumlah kasbon harus berupa angka', + 'jumlah_kasbon.min' => 'Jumlah kasbon minimal 0', + 'tanggal_kasbon.required' => 'Tanggal kasbon harus diisi', + 'tanggal_kasbon.date' => 'Format tanggal kasbon tidak valid', + 'status.required' => 'Status harus dipilih', + 'status.in' => 'Status harus lunas atau belum_lunas', + 'keterangan.max' => 'Keterangan maksimal 500 karakter' + ]); + + $validator->after(function ($validator) use ($request) { + $jumlah = (float) $request->input('jumlah_kasbon'); + $idTeknisi = $request->input('id_teknisi'); + $tanggal = $request->input('tanggal_kasbon'); + + if ($jumlah > 0 && $jumlah <= 500000) { + // Aturan 1: Minimal Rp 200.000 untuk Kasbon Rutin + if ($jumlah < 200000) { + $validator->errors()->add('jumlah_kasbon', 'Jumlah kasbon rutin minimal Rp 200.000. Di atas Rp 500.000 dianggap pinjaman besar.'); + } + + // Aturan 2: Maksimal 2 kali kasbon rutin dalam 1 minggu kalender + if ($idTeknisi && $tanggal) { + try { + $date = Carbon::parse($tanggal); + $startOfWeek = $date->copy()->startOfWeek()->toDateString(); + $endOfWeek = $date->copy()->endOfWeek()->toDateString(); + + $kasbonCount = Kasbon::where('id_teknisi', $idTeknisi) + ->where('jumlah_kasbon', '<=', 500000) + ->whereBetween('tanggal_kasbon', [$startOfWeek, $endOfWeek]) + ->count(); + + if ($kasbonCount >= 2) { + $validator->errors()->add('tanggal_kasbon', 'Teknisi ini sudah mencapai batas maksimal 2 kali kasbon rutin dalam minggu ini (Senin - Minggu).'); + } + } catch (\Exception $e) { + // Let built-in date validator handle formatting errors + } + } + } + }); + + if ($validator->fails()) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'errors' => $validator->errors(), + 'message' => $validator->errors()->first() + ], 422); + } + return redirect()->back()->withErrors($validator)->withInput(); + } + + try { + $data = $validator->validated(); + + // Map keterangan ke keperluan (database schema) + if (isset($data['keterangan'])) { + $data['keperluan'] = $data['keterangan']; + unset($data['keterangan']); + } + + // Set default status jika tidak ada + if (!isset($data['status'])) { + $data['status'] = Kasbon::STATUS_BELUM_LUNAS; + } + + $kasbon = Kasbon::create($data); + + if ($request->expectsJson()) { + return response()->json([ + 'success' => true, + 'data' => $kasbon, + 'message' => 'Kasbon berhasil ditambahkan' + ], 201); + } + + return redirect()->route('kasbon.index')->with('success', 'Kasbon berhasil ditambahkan'); + } catch (\Exception $e) { + \Illuminate\Support\Facades\Log::error('Kasbon Store Error', [ + 'message' => $e->getMessage(), + 'data' => $request->all(), + 'trace' => $e->getTraceAsString() + ]); + + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Terjadi kesalahan saat menyimpan data: ' . $e->getMessage() + ], 500); + } + + return redirect()->back()->with('error', 'Terjadi kesalahan saat menyimpan data: ' . $e->getMessage())->withInput(); + } + } + + /** + * Display the specified resource. + * + * @param int $id + * @param Request $request + * @return View|JsonResponse + */ + public function show(int $id, Request $request) + { + try { + $kasbon = Kasbon::findOrFail($id); + + if ($request->expectsJson()) { + return response()->json([ + 'success' => true, + 'data' => $kasbon, + 'message' => 'Data kasbon berhasil diambil' + ]); + } + + return view('Admin.Gaji.show-kasbon', compact('kasbon')); // DIPERBAIKI: path view + } catch (\Exception $e) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Kasbon tidak ditemukan' + ], 404); + } + + return redirect()->route('kasbon.index')->with('error', 'Kasbon tidak ditemukan'); + } + } + + /** + * Show the form for editing the specified resource. + * + * @param int $id + * @return View|RedirectResponse + */ + public function edit(int $id, Request $request) + { + try { + $kasbon = Kasbon::with('teknisi')->findOrFail($id); + $statusOptions = Kasbon::getStatusOptions(); + $teknisis = Teknisi::orderBy('nama')->get(); + + if ($request->expectsJson()) { + return response()->json([ + 'success' => true, + 'kasbon' => $kasbon, + 'statusOptions' => $statusOptions, + 'teknisis' => $teknisis + ]); + } + + return view('Admin.Gaji.edit-kasbon', compact('kasbon', 'statusOptions', 'teknisis')); + } catch (\Exception $e) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Kasbon tidak ditemukan' + ], 404); + } + return redirect()->route('kasbon.index')->with('error', 'Kasbon tidak ditemukan'); + } + } + + /** + * Update the specified resource in storage. + * + * @param Request $request + * @param int $id + * @return RedirectResponse|JsonResponse + */ + public function update(Request $request, int $id) + { + $validator = Validator::make($request->all(), [ + 'id_teknisi' => 'required|integer|min:1', + 'jumlah_kasbon' => 'required|numeric|min:0', + 'tanggal_kasbon' => 'required|date', + 'status' => 'nullable|in:lunas,belum_lunas', + 'keterangan' => 'nullable|string|max:100' + ], [ + 'id_teknisi.required' => 'ID Teknisi harus diisi', + 'id_teknisi.integer' => 'ID Teknisi harus berupa angka', + 'jumlah_kasbon.required' => 'Jumlah kasbon harus diisi', + 'jumlah_kasbon.numeric' => 'Jumlah kasbon harus berupa angka', + 'jumlah_kasbon.min' => 'Jumlah kasbon minimal 0', + 'tanggal_kasbon.required' => 'Tanggal kasbon harus diisi', + 'tanggal_kasbon.date' => 'Format tanggal kasbon tidak valid', + 'status.required' => 'Status harus dipilih', + 'status.in' => 'Status harus lunas atau belum_lunas', + 'keterangan.max' => 'Keterangan maksimal 500 karakter' + ]); + + $validator->after(function ($validator) use ($request, $id) { + $jumlah = (float) $request->input('jumlah_kasbon'); + $idTeknisi = $request->input('id_teknisi'); + $tanggal = $request->input('tanggal_kasbon'); + + if ($jumlah > 0 && $jumlah <= 500000) { + // Aturan 1: Minimal Rp 200.000 untuk Kasbon Rutin + if ($jumlah < 200000) { + $validator->errors()->add('jumlah_kasbon', 'Jumlah kasbon rutin minimal Rp 200.000. Di atas Rp 500.000 dianggap pinjaman besar.'); + } + + // Aturan 2: Maksimal 2 kali kasbon rutin dalam 1 minggu kalender + if ($idTeknisi && $tanggal) { + try { + $date = Carbon::parse($tanggal); + $startOfWeek = $date->copy()->startOfWeek()->toDateString(); + $endOfWeek = $date->copy()->endOfWeek()->toDateString(); + + $kasbonCount = Kasbon::where('id_teknisi', $idTeknisi) + ->where('id_kasbon', '!=', $id) + ->where('jumlah_kasbon', '<=', 500000) + ->whereBetween('tanggal_kasbon', [$startOfWeek, $endOfWeek]) + ->count(); + + if ($kasbonCount >= 2) { + $validator->errors()->add('tanggal_kasbon', 'Teknisi ini sudah mencapai batas maksimal 2 kali kasbon rutin dalam minggu ini (Senin - Minggu).'); + } + } catch (\Exception $e) { + // Let built-in date validator handle formatting errors + } + } + } + }); + + if ($validator->fails()) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'errors' => $validator->errors(), + 'message' => $validator->errors()->first() + ], 422); + } + return redirect()->back()->withErrors($validator)->withInput(); + } + + try { + $kasbon = Kasbon::findOrFail($id); + $data = $validator->validated(); + + // Map keterangan ke keperluan + if (isset($data['keterangan'])) { + $data['keperluan'] = $data['keterangan']; + unset($data['keterangan']); + } + + $kasbon->update($data); + + if ($request->expectsJson()) { + return response()->json([ + 'success' => true, + 'data' => $kasbon, + 'message' => 'Kasbon berhasil diupdate' + ]); + } + + return redirect()->route('kasbon.index')->with('success', 'Kasbon berhasil diupdate'); + } catch (\Exception $e) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Kasbon tidak ditemukan atau terjadi kesalahan' + ], 404); + } + + return redirect()->route('kasbon.index')->with('error', 'Kasbon tidak ditemukan atau terjadi kesalahan'); + } + } + + /** + * Remove the specified resource from storage. + * + * @param Request $request + * @param int $id + * @return RedirectResponse|JsonResponse + */ + public function destroy(Request $request, int $id) + { + try { + $kasbon = Kasbon::findOrFail($id); + $kasbon->delete(); + + if ($request->expectsJson()) { + return response()->json([ + 'success' => true, + 'message' => 'Kasbon berhasil dihapus' + ]); + } + + return redirect()->route('kasbon.index')->with('success', 'Kasbon berhasil dihapus'); + } catch (\Exception $e) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Kasbon tidak ditemukan atau terjadi kesalahan' + ], 404); + } + + return redirect()->route('kasbon.index')->with('error', 'Kasbon tidak ditemukan atau terjadi kesalahan'); + } + } + + /** + * Get kasbon statistics + * + * @param Request $request + * @return JsonResponse + */ + public function statistics(Request $request): JsonResponse + { + try { + $totalKasbon = Kasbon::count(); + $totalJumlahKasbon = Kasbon::sum('jumlah_kasbon'); + $kasbonLunas = Kasbon::lunas()->count(); + $kasbonBelumLunas = Kasbon::belumLunas()->count(); + $totalJumlahLunas = Kasbon::lunas()->sum('jumlah_kasbon'); + $totalJumlahBelumLunas = Kasbon::belumLunas()->sum('jumlah_kasbon'); + + return response()->json([ + 'success' => true, + 'data' => [ + 'total_kasbon' => $totalKasbon, + 'total_jumlah_kasbon' => $totalJumlahKasbon, + 'kasbon_lunas' => $kasbonLunas, + 'kasbon_belum_lunas' => $kasbonBelumLunas, + 'total_jumlah_lunas' => $totalJumlahLunas, + 'total_jumlah_belum_lunas' => $totalJumlahBelumLunas, + ], + 'message' => 'Statistik kasbon berhasil diambil' + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Terjadi kesalahan saat mengambil statistik' + ], 500); + } + } + + /** + * Update status kasbon to lunas + * + * @param Request $request + * @param int $id + * @return RedirectResponse|JsonResponse + */ + public function markAsLunas(Request $request, int $id) + { + try { + $kasbon = Kasbon::findOrFail($id); + $kasbon->update(['status' => Kasbon::STATUS_LUNAS]); + + if ($request->expectsJson()) { + return response()->json([ + 'success' => true, + 'data' => $kasbon, + 'message' => 'Kasbon berhasil diubah menjadi lunas' + ]); + } + + return redirect()->back()->with('success', 'Kasbon berhasil diubah menjadi lunas'); + } catch (\Exception $e) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Kasbon tidak ditemukan' + ], 404); + } + + return redirect()->back()->with('error', 'Kasbon tidak ditemukan'); + } + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Http/Controllers/LaporanController.php b/samooapk/laravel/app/Http/Controllers/LaporanController.php new file mode 100644 index 0000000..b9fedc6 --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/LaporanController.php @@ -0,0 +1,339 @@ +leftJoin('teknisis', 'kasbons.id_teknisi', '=', 'teknisis.id_teknisi') + ->select('kasbons.*', 'teknisis.nama as nama_teknisi') + ->orderBy('kasbons.created_at', 'desc') + ->limit(5)->get(); + + $recentAbsensi = DB::table('absensis') + ->leftJoin('teknisis', 'absensis.id_teknisi', '=', 'teknisis.id_teknisi') + ->select('absensis.*', 'teknisis.nama as nama_teknisi') + ->orderBy('absensis.tanggal', 'desc') + ->limit(5)->get(); + + $recentPekerjaan = DB::table('penugasans') + ->leftJoin('teknisis', 'penugasans.id_teknisi', '=', 'teknisis.id_teknisi') + ->select('penugasans.*', 'teknisis.nama as nama_teknisi') + ->orderBy('penugasans.created_at', 'desc') + ->limit(5)->get(); + + return view('Admin.Laporan', compact( + 'statistics', 'recentKasbon', 'recentAbsensi', 'recentPekerjaan' + )); + } catch (Exception $e) { + \Log::error('Laporan Error: ' . $e->getMessage()); + return back()->with('error', 'Gagal memuat laporan: ' . $e->getMessage()); + } + } + + /** + * Get statistics for AJAX refresh + */ + public function statistics() + { + return response()->json([ + 'success' => true, + 'data' => Laporan::getStatistics() + ]); + } + + /** + * Detailed Kasbon Report + */ + public function kasbon(Request $request) + { + $filters = $request->only(['tanggal_dari', 'tanggal_sampai', 'status', 'id_teknisi', 'search']); + $data = Laporan::getKasbonData($filters)->paginate(15)->appends($filters); + $teknisis = Teknisi::orderBy('nama')->get(); + + // Statistik ringkas kasbon + $statsKasbon = [ + 'total' => DB::table('kasbons')->count(), + 'lunas' => DB::table('kasbons')->where('status', 'lunas')->count(), + 'belum_lunas' => DB::table('kasbons')->where('status', 'belum_lunas')->count(), + 'total_nominal' => DB::table('kasbons')->sum('jumlah_kasbon'), + 'total_belum_lunas' => DB::table('kasbons')->where('status', 'belum_lunas')->sum('jumlah_kasbon'), + ]; + + return view('Admin.Laporan.kasbon', compact('data', 'teknisis', 'filters', 'statsKasbon')); + } + + /** + * Detailed Teknisi/Kehadiran Report + */ + public function teknisi(Request $request) + { + $filters = $request->only(['tanggal_dari', 'tanggal_sampai', 'search']); + $data = Laporan::getTeknisiData($filters)->get(); + return view('Admin.Laporan.teknisi', compact('data', 'filters')); + } + + /** + * Detailed Absensi Report + */ + public function absensi(Request $request) + { + $filters = $request->only(['tanggal_dari', 'tanggal_sampai', 'status', 'id_teknisi', 'search', 'tanggal']); + if (!empty($filters['tanggal'])) { + $filters['tanggal_dari'] = $filters['tanggal']; + $filters['tanggal_sampai'] = $filters['tanggal']; + } + $data = Laporan::getAbsensiData($filters)->paginate(15)->appends($filters); + $teknisis = Teknisi::orderBy('nama')->get(); + + // Statistik absensi bulan ini + $statsAbsensi = [ + 'hadir' => DB::table('absensis')->where('status', 'hadir')->whereMonth('tanggal', date('m'))->whereYear('tanggal', date('Y'))->count(), + 'izin' => DB::table('absensis')->where('status', 'izin')->whereMonth('tanggal', date('m'))->whereYear('tanggal', date('Y'))->count(), + 'sakit' => DB::table('absensis')->where('status', 'sakit')->whereMonth('tanggal', date('m'))->whereYear('tanggal', date('Y'))->count(), + 'alpha' => DB::table('absensis')->where('status', 'alpha')->whereMonth('tanggal', date('m'))->whereYear('tanggal', date('Y'))->count(), + ]; + + return view('Admin.Laporan.absensi', compact('data', 'teknisis', 'filters', 'statsAbsensi')); + } + + /** + * Detailed Pekerjaan Report + */ + public function pekerjaan(Request $request) + { + $filters = $request->only(['tanggal_dari', 'tanggal_sampai', 'status', 'id_teknisi', 'search', 'tanggal']); + if (!empty($filters['tanggal'])) { + $filters['tanggal_dari'] = $filters['tanggal']; + $filters['tanggal_sampai'] = $filters['tanggal']; + } + $data = Laporan::getPekerjaanData($filters)->paginate(15)->appends($filters); + $teknisis = Teknisi::orderBy('nama')->get(); + + $statsPekerjaan = [ + 'total' => DB::table('penugasans')->count(), + 'selesai' => DB::table('penugasans')->where('status_pekerjaan', 'selesai')->count(), + 'proses' => DB::table('penugasans')->where('status_pekerjaan', 'proses')->count(), + 'pending' => DB::table('penugasans')->where('status_pekerjaan', 'pending')->count(), + ]; + + return view('Admin.Laporan.pekerjaan', compact('data', 'teknisis', 'filters', 'statsPekerjaan')); + } + + // ============================================================ + // LAPORAN PENGGAJIAN & DATA TEKNISI + // ============================================================ + + public function penggajian(Request $request) + { + $filters = $request->only(['tanggal_dari', 'tanggal_sampai', 'status', 'id_teknisi', 'search', 'tanggal']); + if (!empty($filters['tanggal'])) { + $filters['tanggal_dari'] = $filters['tanggal']; + $filters['tanggal_sampai'] = $filters['tanggal']; + } + + $query = DB::table('penggajians') + ->leftJoin('teknisis', 'penggajians.id_teknisi', '=', 'teknisis.id_teknisi') + ->select('penggajians.*', 'teknisis.nama as nama_teknisi') + ->orderBy('penggajians.created_at', 'desc'); + + if (!empty($filters['id_teknisi'])) { + $query->where('penggajians.id_teknisi', $filters['id_teknisi']); + } + if (!empty($filters['status'])) { + $query->where('penggajians.status_pembayaran', $filters['status']); + } + if (!empty($filters['tanggal_dari'])) { + $query->whereDate('penggajians.created_at', '>=', $filters['tanggal_dari']); + } + if (!empty($filters['tanggal_sampai'])) { + $query->whereDate('penggajians.created_at', '<=', $filters['tanggal_sampai']); + } + + $data = $query->paginate(15)->appends($filters); + $teknisis = Teknisi::orderBy('nama')->get(); + + $statsPenggajian = [ + 'total' => DB::table('penggajians')->count(), + 'lunas' => DB::table('penggajians')->where('status_pembayaran', 'lunas')->count(), + 'belum' => DB::table('penggajians')->where('status_pembayaran', 'belum_bayar')->count(), + ]; + + return view('Admin.Laporan.penggajian', compact('data', 'teknisis', 'filters', 'statsPenggajian')); + } + + public function dataTeknisi(Request $request) + { + $filters = $request->only(['status', 'search']); + $query = Teknisi::query(); + if (!empty($filters['search'])) { + $query->where('nama', 'like', '%'.$filters['search'].'%'); + } + if (!empty($filters['status'])) { + $query->where('status', $filters['status']); + } + $data = $query->paginate(15)->appends($filters); + + $statsTeknisi = [ + 'total' => Teknisi::count(), + 'aktif' => Teknisi::where('status', 'aktif')->count(), + 'nonaktif' => Teknisi::where('status', 'nonaktif')->count(), + ]; + + return view('Admin.Laporan.data_teknisi', compact('data', 'filters', 'statsTeknisi')); + } + + // ============================================================ + // CETAK LAPORAN (PRINT) METHODS + // ============================================================ + + /** + * Cetak Laporan Kasbon + */ + public function exportKasbon(Request $request) + { + $filters = $request->only(['tanggal_dari', 'tanggal_sampai', 'status', 'id_teknisi', 'search']); + $data = Laporan::getKasbonData($filters)->get(); + $title = 'Kasbon Teknisi'; + $type = 'kasbon'; + $columns = ['Nama Teknisi', 'Tanggal Pinjam', 'Jumlah Kasbon', 'Status', 'Keperluan']; + + return view('Admin.Laporan.print', compact('data', 'filters', 'title', 'type', 'columns')); + } + + /** + * Cetak Laporan Teknisi/Kehadiran + */ + public function exportTeknisi(Request $request) + { + $filters = $request->only(['tanggal_dari', 'tanggal_sampai', 'search']); + $data = Laporan::getTeknisiData($filters)->get(); + $title = 'Performa & Kehadiran Teknisi'; + $type = 'teknisi'; + $columns = ['Nama Teknisi', 'Status', 'Hadir', 'Izin', 'Sakit', 'Alpha', 'Total Hari', 'Persentase']; + + return view('Admin.Laporan.print', compact('data', 'filters', 'title', 'type', 'columns')); + } + + /** + * Cetak Laporan Absensi + */ + public function exportAbsensi(Request $request) + { + $filters = $request->only(['tanggal_dari', 'tanggal_sampai', 'status', 'id_teknisi', 'search']); + $data = Laporan::getAbsensiData($filters)->get(); + $title = 'Absensi Harian'; + $type = 'absensi'; + $columns = ['Nama Teknisi', 'Tanggal', 'Status Kehadiran', 'Keterangan']; + + return view('Admin.Laporan.print', compact('data', 'filters', 'title', 'type', 'columns')); + } + + /** + * Cetak Laporan Pekerjaan + */ + public function exportPekerjaan(Request $request) + { + $filters = $request->only(['tanggal_dari', 'tanggal_sampai', 'status', 'id_teknisi', 'search']); + $data = Laporan::getPekerjaanData($filters)->get(); + $title = 'Pekerjaan Teknisi'; + $type = 'pekerjaan'; + $columns = ['ID', 'Jenis Pekerjaan', 'Nama Teknisi', 'Status', 'Tgl Mulai', 'Tgl Selesai']; + + return view('Admin.Laporan.print', compact('data', 'filters', 'title', 'type', 'columns')); + } + + /** + * Cetak Laporan Penggajian + */ + public function exportPenggajian(Request $request) + { + $filters = $request->only(['tanggal_dari', 'tanggal_sampai', 'status', 'id_teknisi', 'search', 'tanggal']); + if (!empty($filters['tanggal'])) { + $filters['tanggal_dari'] = $filters['tanggal']; + $filters['tanggal_sampai'] = $filters['tanggal']; + } + + $query = DB::table('penggajians') + ->leftJoin('teknisis', 'penggajians.id_teknisi', '=', 'teknisis.id_teknisi') + ->select('penggajians.*', 'teknisis.nama as nama_teknisi') + ->orderBy('penggajians.created_at', 'desc'); + + if (!empty($filters['id_teknisi'])) { + $query->where('penggajians.id_teknisi', $filters['id_teknisi']); + } + if (!empty($filters['status'])) { + $query->where('penggajians.status_pembayaran', $filters['status']); + } + if (!empty($filters['tanggal_dari'])) { + $query->whereDate('penggajians.created_at', '>=', $filters['tanggal_dari']); + } + if (!empty($filters['tanggal_sampai'])) { + $query->whereDate('penggajians.created_at', '<=', $filters['tanggal_sampai']); + } + + $data = $query->get(); + $title = 'Laporan Penggajian'; + $type = 'penggajian'; + $columns = ['Nama Teknisi', 'Periode', 'Total Gaji', 'Status Pembayaran']; + + return view('Admin.Laporan.print', compact('data', 'filters', 'title', 'type', 'columns')); + } + + /** + * Cetak Laporan Data Teknisi + */ + public function exportDataTeknisi(Request $request) + { + $filters = $request->only(['status', 'search']); + $query = Teknisi::query(); + if (!empty($filters['search'])) { + $query->where('nama', 'like', '%'.$filters['search'].'%'); + } + if (!empty($filters['status'])) { + $query->where('status', $filters['status']); + } + + $data = $query->get(); + $title = 'Data Teknisi'; + $type = 'data_teknisi'; + $columns = ['Nama Teknisi', 'Email', 'No Telepon', 'Tanggal Masuk', 'Status']; + + return view('Admin.Laporan.print', compact('data', 'filters', 'title', 'type', 'columns')); + } + + /** + * Generic export (from Ringkasan page) + */ + public function export(Request $request) + { + $jenis = $request->get('jenis_laporan', 'kasbon'); + + switch ($jenis) { + case 'kasbon': return $this->exportKasbon($request); + case 'teknisi': return $this->exportTeknisi($request); + case 'absensi': return $this->exportAbsensi($request); + case 'pekerjaan': return $this->exportPekerjaan($request); + case 'penggajian': return $this->exportPenggajian($request); + case 'data_teknisi': return $this->exportDataTeknisi($request); + default: return back()->with('error', 'Jenis laporan tidak valid.'); + } + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Http/Controllers/PenggajianController.php b/samooapk/laravel/app/Http/Controllers/PenggajianController.php new file mode 100644 index 0000000..4a718e1 --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/PenggajianController.php @@ -0,0 +1,907 @@ +latest('periode_tahun') + ->latest('periode_bulan'); + + if ($request->filled('periode_bulan')) { + $query->where('periode_bulan', $request->periode_bulan); + } + if ($request->filled('periode_tahun')) { + $query->where('periode_tahun', $request->periode_tahun); + } + if ($request->filled('status_pembayaran')) { + $query->where('status_pembayaran', $request->status_pembayaran); + } + if ($request->filled('id_teknisi')) { + $query->where('id_teknisi', $request->id_teknisi); + } + + if ($request->expectsJson()) { + $penggajians = $query->get(); + $rows = $penggajians->map(function ($g) { + return [ + 'id_penggajian' => $g->id_penggajian, + 'nama_teknisi' => $g->teknisi->nama ?? 'N/A', + 'periode_label' => Carbon::create()->month($g->periode_bulan)->format('M') . ' ' . $g->periode_tahun, + 'tanggal_penggajian' => $g->tanggal_penggajian->format('d/m/Y'), + 'jumlah_hari_kerja' => $g->jumlah_hari_kerja, + 'total_ongkos_pekerjaan' => $g->total_ongkos_pekerjaan, + 'biaya_makan' => $g->biaya_makan, + 'total_kasbon' => $g->total_kasbon, + 'gaji_bersih' => $g->gaji_bersih, + 'status_pembayaran' => $g->status_pembayaran, + ]; + }); + $summary = $penggajians->count() > 0 ? [ + 'total_teknisi' => $penggajians->count(), + 'total_gaji' => $penggajians->sum('total_ongkos_pekerjaan'), + 'total_kasbon' => $penggajians->sum('total_kasbon') + $penggajians->sum('biaya_makan'), + 'gaji_bersih' => $penggajians->sum('gaji_bersih'), + ] : null; + return response()->json(['rows' => $rows, 'summary' => $summary]); + } + + $teknisiList = Teknisi::where('status', 'aktif')->orderBy('nama')->get(); + return view('Admin.Gaji.Penggajian', compact('teknisiList')); + } + + /** + * Show the form for creating a new resource. + */ + public function create() + { + $teknisis = Teknisi::where('status', 'aktif')->get(); + $tarifs = TarifPekerjaan::where('is_active', true)->get(); + return view('admin.penggajian.create', compact('teknisis', 'tarifs')); + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'id_teknisi' => 'required|exists:teknisis,id_teknisi', + 'periode_bulan' => 'required|integer|between:1,12', + 'periode_tahun' => 'required|integer|min:2020|max:' . (date('Y') + 1), + 'tanggal_penggajian' => 'required|date', + 'biaya_makan' => 'nullable|numeric|min:0', + 'total_kasbon' => 'nullable|numeric|min:0', + 'details' => 'required|array|min:1', + 'details.*.id_tarif' => 'required|exists:tarif_pekerjaans,id_tarif', + 'details.*.jumlah_unit' => 'required|integer|min:1', + 'details.*.tarif_per_unit' => 'required|numeric|min:0', + ]); + + if ($validator->fails()) { + return back()->withErrors($validator)->withInput(); + } + + $exists = Penggajian::isPeriodeExists( + $request->id_teknisi, + $request->periode_bulan, + $request->periode_tahun + ); + + if ($exists) { + return back()->withErrors([ + 'periode' => 'Penggajian untuk teknisi ini pada periode tersebut sudah ada.' + ])->withInput(); + } + + DB::beginTransaction(); + try { + $gaji_kotor = 0; + foreach ($request->details as $detail) { + $gaji_kotor += $detail['jumlah_unit'] * $detail['tarif_per_unit']; + } + + $penggajian = Penggajian::create([ + 'id_teknisi' => $request->id_teknisi, + 'periode_bulan' => $request->periode_bulan, + 'periode_tahun' => $request->periode_tahun, + 'tanggal_penggajian' => $request->tanggal_penggajian, + 'jumlah_hari_kerja' => $this->calculateHariKerja( + $request->id_teknisi, + $request->periode_bulan, + $request->periode_tahun + ), + 'gaji_kotor' => $gaji_kotor, + 'biaya_makan' => $request->biaya_makan ?? 0, + 'total_kasbon' => $request->total_kasbon ?? 0, + 'total_potongan' => $request->total_kasbon ?? 0, + 'status_pembayaran' => Penggajian::STATUS_BELUM_BAYAR, + ]); + + foreach ($request->details as $detail) { + DetailPenggajian::create([ + 'id_penggajian' => $penggajian->id_penggajian, + 'id_teknisi' => $request->id_teknisi, + 'id_tarif' => $detail['id_tarif'], + 'jumlah_unit' => $detail['jumlah_unit'], + 'tarif_per_unit' => $detail['tarif_per_unit'], + ]); + } + + DB::commit(); + return redirect()->route('penggajian.index') + ->with('success', 'Data penggajian berhasil dibuat.'); + + } catch (\Exception $e) { + DB::rollback(); + return back()->withErrors(['error' => 'Gagal menyimpan data penggajian.'])->withInput(); + } + } + + /** + * Display the specified resource. + */ + public function show(Penggajian $penggajian) + { + $penggajian->load(['teknisi', 'detailPenggajian.penugasan']); + return view('admin.penggajian.show', compact('penggajian')); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(Penggajian $penggajian) + { + $penggajian->load(['teknisi', 'detailPenggajian.penugasan']); + $teknisis = Teknisi::where('status', 'aktif')->get(); + $tarifs = TarifPekerjaan::where('is_active', true)->get(); + return view('admin.penggajian.edit', compact('penggajian', 'teknisis', 'tarifs')); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, Penggajian $penggajian) + { + $validator = Validator::make($request->all(), [ + 'tanggal_penggajian' => 'required|date', + 'biaya_makan' => 'nullable|numeric|min:0', + 'total_kasbon' => 'nullable|numeric|min:0', + 'details' => 'required|array|min:1', + 'details.*.id_tarif' => 'required|exists:tarif_pekerjaans,id_tarif', + 'details.*.jumlah_unit' => 'required|integer|min:1', + 'details.*.tarif_per_unit' => 'required|numeric|min:0', + ]); + + if ($validator->fails()) { + return back()->withErrors($validator)->withInput(); + } + + DB::beginTransaction(); + try { + $gaji_kotor = 0; + foreach ($request->details as $detail) { + $gaji_kotor += $detail['jumlah_unit'] * $detail['tarif_per_unit']; + } + + $penggajian->update([ + 'tanggal_penggajian' => $request->tanggal_penggajian, + 'total_ongkos_pekerjaan' => $gaji_kotor, + 'biaya_makan' => $request->biaya_makan ?? 0, + 'total_kasbon' => $request->total_kasbon ?? 0, + 'total_potongan' => $request->total_kasbon ?? 0, + ]); + + $penggajian->detailPenggajian()->delete(); + + $gajiData = $this->calculateGajiTeknisi( + $penggajian->id_teknisi, + $penggajian->periode_bulan, + $penggajian->periode_tahun, + $penggajian->tanggal_penggajian + ); + + foreach ($gajiData['details'] as $detail) { + DetailPenggajian::create([ + 'id_penggajian' => $penggajian->id_penggajian, + 'id_penugasan' => $detail['id_penugasan'], + 'tanggal_selesai' => $detail['tanggal_selesai'], + 'lokasi' => $detail['lokasi'], + 'ongkos_penugasan' => $detail['ongkos_penugasan'], + 'jumlah_tim' => $detail['jumlah_tim'], + 'bagian_ongkos' => $detail['bagian_ongkos'], + ]); + } + + DB::commit(); + return redirect()->route('penggajian.index') + ->with('success', 'Data penggajian berhasil diperbarui.'); + + } catch (\Exception $e) { + DB::rollback(); + return back()->withErrors(['error' => 'Gagal memperbarui data penggajian.'])->withInput(); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Penggajian $penggajian) + { + try { + DB::beginTransaction(); + $penggajian->detailPenggajian()->delete(); + $penggajian->delete(); + DB::commit(); + + return response()->json(['success' => true, 'message' => 'Data penggajian berhasil dihapus.']); + } catch (\Exception $e) { + DB::rollback(); + return response()->json(['success' => false, 'message' => 'Gagal menghapus data penggajian.'], 500); + } + } + + /** + * Hitung gaji untuk periode tertentu. + * + * Tambahan parameter: + * force_recalculate (boolean, default false) + * → Jika true, data penggajian yang sudah ada akan dihapus dan dihitung ulang. + * Gunakan ini ketika ada penugasan baru yang selesai SETELAH gaji di-generate, + * sehingga semua anggota tim (termasuk yang tidak jadi teknisi utama) ikut terhitung. + * → Jika false (default), teknisi yang sudah punya data di periode itu akan di-skip. + * + * CATATAN: Penggajian yang sudah berstatus 'sudah_bayar' TIDAK akan di-recalculate + * meski force_recalculate = true, untuk mencegah perubahan data yang sudah dibayar. + */ + public function hitungGaji(Request $request) + { + $validator = Validator::make($request->all(), [ + 'id_teknisi' => 'nullable|exists:teknisis,id_teknisi', + 'periode_bulan' => 'required|integer|between:1,12', + 'periode_tahun' => 'required|integer|min:2020|max:' . (date('Y') + 1), + 'tanggal_penggajian' => 'required|date', + 'include_kasbon' => 'boolean', + 'force_recalculate' => 'boolean', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Data tidak valid. ' . implode(' ', $validator->errors()->all()), + 'errors' => $validator->errors() + ], 422); + } + + try { + DB::beginTransaction(); + + $periode_bulan = $request->periode_bulan; + $periode_tahun = $request->periode_tahun; + $tanggal_penggajian = $request->tanggal_penggajian + ?? Carbon::create($periode_tahun, $periode_bulan)->endOfMonth()->format('Y-m-d'); + $include_kasbon = $request->boolean('include_kasbon', true); + $force_recalculate = $request->boolean('force_recalculate', false); + $id_teknisi = $request->id_teknisi; + + if ($id_teknisi) { + $teknisis = Teknisi::where('id_teknisi', $id_teknisi)->where('status', 'aktif')->get(); + } else { + $teknisis = Teknisi::where('status', 'aktif')->get(); + } + + $created_count = 0; + $skipped_count = 0; + $recalculated_count = 0; + + foreach ($teknisis as $teknisi) { + // Cek apakah sudah ada penggajian untuk periode ini + $existingPenggajian = Penggajian::where('id_teknisi', $teknisi->id_teknisi) + ->where('periode_bulan', $periode_bulan) + ->where('periode_tahun', $periode_tahun) + ->first(); + + if ($existingPenggajian) { + if (!$force_recalculate) { + // Tidak force → skip seperti perilaku lama + $skipped_count++; + continue; + } + + // Sudah dibayar → jangan recalculate, lindungi data yang sudah final + if ($existingPenggajian->status_pembayaran === Penggajian::STATUS_SUDAH_BAYAR) { + $skipped_count++; + continue; + } + + // Force recalculate → hapus detail dan penggajian lama, hitung ulang + $existingPenggajian->detailPenggajian()->delete(); + $existingPenggajian->delete(); + $recalculated_count++; + } + + $gajiData = $this->calculateGajiTeknisi( + $teknisi->id_teknisi, + $periode_bulan, + $periode_tahun, + $tanggal_penggajian, + $include_kasbon + ); + + if ($gajiData['gaji_kotor'] > 0 || $gajiData['biaya_makan'] > 0 || !empty($gajiData['details'])) { + $total_penugasan = !empty($gajiData['details']) ? count($gajiData['details']) : 0; + + $penggajian = Penggajian::create([ + 'id_teknisi' => $teknisi->id_teknisi, + 'periode_bulan' => $periode_bulan, + 'periode_tahun' => $periode_tahun, + 'tanggal_penggajian' => $tanggal_penggajian, + 'jumlah_hari_kerja' => $gajiData['jumlah_hari_kerja'], + 'jumlah_penugasan_selesai' => $total_penugasan, + 'total_ongkos_pekerjaan' => $gajiData['gaji_kotor'], + 'biaya_makan' => $gajiData['biaya_makan'], + 'total_kasbon' => $gajiData['total_kasbon'], + 'total_potongan' => $gajiData['total_kasbon'], + 'gaji_bersih' => $gajiData['gaji_kotor'] - $gajiData['biaya_makan'] - $gajiData['total_kasbon'], + 'status_pembayaran' => Penggajian::STATUS_BELUM_BAYAR, + ]); + + foreach ($gajiData['details'] as $detail) { + DetailPenggajian::create([ + 'id_penggajian' => $penggajian->id_penggajian, + 'id_penugasan' => $detail['id_penugasan'], + 'tanggal_selesai' => $detail['tanggal_selesai'], + 'lokasi' => $detail['lokasi'], + 'ongkos_penugasan' => $detail['ongkos_penugasan'], + 'jumlah_tim' => $detail['jumlah_tim'], + 'bagian_ongkos' => $detail['bagian_ongkos'], + 'rincian_pekerjaan'=> $detail['rincian_pekerjaan'] ?? null, + ]); + } + + $created_count++; + } + } + + DB::commit(); + + $message = "Berhasil menghitung gaji untuk {$created_count} teknisi."; + if ($recalculated_count > 0) { + $message .= " {$recalculated_count} teknisi dihitung ulang."; + } + if ($skipped_count > 0) { + $message .= " {$skipped_count} teknisi dilewati (sudah ada data atau sudah dibayar)."; + } + + return response()->json([ + 'success' => true, + 'message' => $message, + 'data' => [ + 'created' => $created_count, + 'recalculated' => $recalculated_count, + 'skipped' => $skipped_count, + ] + ]); + + } catch (\Exception $e) { + DB::rollback(); + return response()->json([ + 'success' => false, + 'message' => 'Gagal menghitung gaji: ' . $e->getMessage() + ], 500); + } + } + + /** + * Hitung ulang penggajian untuk satu teknisi tertentu. + * + * Endpoint: POST /penggajian/{penggajian}/recalculate + * + * Berguna ketika: + * - Ada penugasan tim yang baru selesai setelah gaji di-generate + * - Anggota tim yang tidak jadi teknisi utama tidak muncul di rincian + * - Data detail penggajian tidak lengkap / tidak sesuai + * + * Penggajian yang sudah 'sudah_bayar' tidak bisa di-recalculate. + */ + public function recalculate(Penggajian $penggajian) + { + try { + DB::beginTransaction(); + + // Hapus detail lama + $penggajian->detailPenggajian()->delete(); + + // Jika sudah lunas, kita tidak ingin menarik data kasbon baru dari database + // karena kasbon aslinya mungkin sudah berstatus lunas. + // Kita gunakan nilai total_kasbon yang sudah tersimpan di record ini. + $isPaid = $penggajian->status_pembayaran === Penggajian::STATUS_SUDAH_BAYAR; + + $gajiData = $this->calculateGajiTeknisi( + $penggajian->id_teknisi, + $penggajian->periode_bulan, + $penggajian->periode_tahun, + $penggajian->tanggal_penggajian, + !$isPaid // includeKasbon hanya jika belum bayar + ); + + $total_penugasan = count($gajiData['details']); + + // Update header penggajian + $penggajian->update([ + 'jumlah_hari_kerja' => $gajiData['jumlah_hari_kerja'], + 'jumlah_penugasan_selesai' => $total_penugasan, + 'total_ongkos_pekerjaan' => $gajiData['gaji_kotor'], + 'biaya_makan' => $gajiData['biaya_makan'], + 'total_kasbon' => $isPaid ? $penggajian->total_kasbon : $gajiData['total_kasbon'], + 'total_potongan' => $isPaid ? $penggajian->total_kasbon : $gajiData['total_kasbon'], + 'gaji_bersih' => $gajiData['gaji_kotor'] - $gajiData['biaya_makan'] - ($isPaid ? $penggajian->total_kasbon : $gajiData['total_kasbon']), + ]); + + // Insert detail baru + foreach ($gajiData['details'] as $detail) { + DetailPenggajian::create([ + 'id_penggajian' => $penggajian->id_penggajian, + 'id_penugasan' => $detail['id_penugasan'], + 'tanggal_selesai' => $detail['tanggal_selesai'], + 'lokasi' => $detail['lokasi'], + 'ongkos_penugasan' => $detail['ongkos_penugasan'], + 'jumlah_tim' => $detail['jumlah_tim'], + 'bagian_ongkos' => $detail['bagian_ongkos'], + 'rincian_pekerjaan'=> $detail['rincian_pekerjaan'] ?? null, + ]); + } + + DB::commit(); + + return response()->json([ + 'success' => true, + 'message' => "Penggajian berhasil dihitung ulang. Ditemukan {$total_penugasan} penugasan.", + 'data' => [ + 'total_penugasan' => $total_penugasan, + 'total_ongkos' => $gajiData['gaji_kotor'], + 'jumlah_hari_kerja' => $gajiData['jumlah_hari_kerja'], + 'biaya_makan' => $gajiData['biaya_makan'], + 'total_kasbon' => $gajiData['total_kasbon'], + ] + ]); + + } catch (\Exception $e) { + DB::rollback(); + return response()->json([ + 'success' => false, + 'message' => 'Gagal menghitung ulang: ' . $e->getMessage() + ], 500); + } + } + + /** + * Get detail penggajian for modal + */ + public function detail(Penggajian $penggajian) + { + $penggajian->load(['teknisi', 'detailPenggajian.penugasan']); + return view('Admin.Gaji.detail_penggajian', compact('penggajian')); + } + + /** + * Process payment for specific penggajian + */ + public function prosesPembayaran(Penggajian $penggajian) + { + try { + if ($penggajian->isPaid()) { + return response()->json(['success' => false, 'message' => 'Gaji sudah dibayar sebelumnya.']); + } + + $penggajian->markAsPaid(); + + // ── LOGIKA PELUNASAN KASBON (DENGAN SISTEM CICILAN/PARTIAL) ── + $sisaPotongan = (float) $penggajian->total_kasbon; + + if ($sisaPotongan > 0) { + // Ambil semua kasbon belum lunas, urutkan dari yang paling lama + $kasbons = Kasbon::where('id_teknisi', $penggajian->id_teknisi) + ->where('status', 'belum_lunas') + ->whereDate('tanggal_kasbon', '<=', $penggajian->tanggal_penggajian) + ->orderBy('tanggal_kasbon', 'asc') + ->get(); + + foreach ($kasbons as $kb) { + if ($sisaPotongan <= 0) break; + + $jumlahKb = (float) $kb->jumlah_kasbon; + + if ($sisaPotongan >= $jumlahKb) { + // Kasbon ini terbayar penuh + $kb->update(['status' => 'lunas']); + $sisaPotongan -= $jumlahKb; + } else { + // Kasbon ini terbayar sebagian (CICILAN) + // 1. Kurangi nilai kasbon lama + $kb->update([ + 'jumlah_kasbon' => $jumlahKb - $sisaPotongan + ]); + + // 2. Buat record baru untuk bagian yang sudah lunas (untuk histori) + $newLunas = $kb->replicate(); + $newLunas->jumlah_kasbon = $sisaPotongan; + $newLunas->status = 'lunas'; + $newLunas->keperluan = $kb->keperluan . ' (Cicilan via Gaji ' . $penggajian->formatted_periode . ')'; + $newLunas->save(); + + $sisaPotongan = 0; + } + } + } + + return response()->json([ + 'success' => true, + 'message' => 'Pembayaran berhasil diproses dan kasbon telah dilunasi.' + ]); + + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => 'Gagal memproses pembayaran.'], 500); + } + } + + public function updateKasbon(Request $request, Penggajian $penggajian) + { + try { + $rawInput = $request->input('total_kasbon', 0); + $cleanInput = preg_replace('/[^0-9]/', '', $rawInput); + $newKasbon = (float) $cleanInput; + + if ($newKasbon < 0) { + return response()->json(['success' => false, 'message' => 'Nominal kasbon tidak boleh negatif.'], 422); + } + + $penggajian->update([ + 'total_kasbon' => $newKasbon, + 'total_potongan' => $newKasbon, + 'gaji_bersih' => $penggajian->total_ongkos_pekerjaan - $penggajian->biaya_makan - $newKasbon, + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Potongan kasbon berhasil diubah menjadi Rp ' . number_format($newKasbon, 0, ',', '.') . '. Gaji bersih akan dihitung ulang otomatis.' + ]); + + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => 'Gagal mengubah potongan: ' . $e->getMessage()], 500); + } + } + + /** + * Update food deduction amount manually. + * Allows mandor to waive food costs for bereavement or emergencies. + */ + public function updateMakan(Request $request, Penggajian $penggajian) + { + try { + $rawInput = $request->input('biaya_makan', 0); + $cleanInput = preg_replace('/[^0-9]/', '', $rawInput); + $newMakan = (float) $cleanInput; + + if ($newMakan < 0) { + return response()->json(['success' => false, 'message' => 'Nominal biaya makan tidak boleh negatif.'], 422); + } + + $penggajian->update([ + 'biaya_makan' => $newMakan, + 'gaji_bersih' => $penggajian->total_ongkos_pekerjaan - $newMakan - $penggajian->total_kasbon, + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Biaya makan berhasil diubah menjadi Rp ' . number_format($newMakan, 0, ',', '.') . '.' + ]); + + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => 'Gagal mengubah biaya makan: ' . $e->getMessage()], 500); + } + } + + /** + * Process all unpaid payments + */ + public function prosesSemuaPembayaran() + { + try { + $unpaidCount = Penggajian::belumBayar()->count(); + + if ($unpaidCount == 0) { + return response()->json(['success' => false, 'message' => 'Tidak ada pembayaran yang perlu diproses.']); + } + + Penggajian::belumBayar()->update(['status_pembayaran' => Penggajian::STATUS_SUDAH_BAYAR]); + + return response()->json([ + 'success' => true, + 'message' => "Berhasil memproses pembayaran untuk {$unpaidCount} teknisi." + ]); + + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => 'Gagal memproses pembayaran.'], 500); + } + } + + /** + * Generate slip gaji + */ + public function slip(Penggajian $penggajian) + { + $penggajian->load(['teknisi', 'detailPenggajian.penugasan']); + return view('admin.Gaji.slip_penggajian', compact('penggajian')); + } + + /** + * Export to Excel + */ + public function export(Request $request) + { + $query = Penggajian::with(['teknisi']); + + if ($request->filled('periode_bulan')) $query->where('periode_bulan', $request->periode_bulan); + if ($request->filled('periode_tahun')) $query->where('periode_tahun', $request->periode_tahun); + if ($request->filled('status_pembayaran')) $query->where('status_pembayaran', $request->status_pembayaran); + + $penggajians = $query->get(); + return Excel::download(new PenggajianExport($penggajians), 'penggajian_' . date('Y-m-d') . '.xlsx'); + } + + // ===================================================================== + // PRIVATE HELPERS + // ===================================================================== + + /** + * Calculate gaji untuk teknisi tertentu. + * + * Mencari semua penugasan di mana teknisi ini terlibat, baik sebagai: + * (a) Teknisi utama (kolom id_teknisi di tabel penugasans), ATAU + * (b) Anggota tim (tabel tim_teknisi_penugasans) dengan status_kehadiran = hadir + * + * Ongkos dibagi rata berdasarkan jumlah anggota tim yang hadir. + * Contoh: ongkos Rp 50.000, tim 2 orang → masing-masing Rp 25.000 + */ + private function calculateGajiTeknisi($idTeknisi, $bulan, $tahun, $tanggalLimit = null, $includeKasbon = true) + { + $jumlah_hari_kerja = Absensi::where('id_teknisi', $idTeknisi) + ->whereMonth('tanggal', $bulan) + ->whereYear('tanggal', $tahun) + ->where('status', 'hadir') + ->count(); + + // Cari semua penugasan yang melibatkan teknisi ini di bulan/tahun tsb + $penugasans = Penugasan::where(function ($q) use ($idTeknisi) { + // (a) Teknisi utama penugasan + $q->where('id_teknisi', $idTeknisi) + // (b) Anggota tim yang hadir + ->orWhereHas('timTeknisi', function ($sq) use ($idTeknisi) { + $sq->where('id_teknisi', $idTeknisi) + ->where('status_kehadiran', 'hadir'); + }); + }) + ->where('status_pekerjaan', 'selesai') + ->where(function ($q) use ($bulan, $tahun) { + // Filter berdasarkan bulan selesai + // Prioritas: tanggal_diselesaikan, fallback ke updated_at + $q->where(function ($q2) use ($bulan, $tahun) { + $q2->whereNotNull('tanggal_diselesaikan') + ->whereMonth('tanggal_diselesaikan', $bulan) + ->whereYear('tanggal_diselesaikan', $tahun); + })->orWhere(function ($q2) use ($bulan, $tahun) { + $q2->whereNull('tanggal_diselesaikan') + ->whereMonth('updated_at', $bulan) + ->whereYear('updated_at', $tahun); + }); + }) + ->with(['items.tarif', 'timTeknisi']) + ->get(); + + $gaji_kotor = 0; + $list_penugasan = []; + + foreach ($penugasans as $penugasan) { + // Hitung jumlah anggota tim yang hadir + // Jika tidak ada record tim → teknisi kerja sendiri → jumlah = 1 + $jumlahHadir = $penugasan->countTimHadir(); + if ($jumlahHadir === 0) { + $jumlahHadir = 1; + } + + // Hitung total ongkos dari penugasan_items + $totalOngkosTugas = 0; + if ($penugasan->items->count() > 0) { + foreach ($penugasan->items as $item) { + $itemTotal = (float) $item->total_nilai_pekerjaan; + if ($itemTotal <= 0) { + $itemTotal = $this->calculatePenugasanItemValue($item); + } + $totalOngkosTugas += $itemTotal; + } + } + + // Fallback: ambil dari total_nilai_pekerjaan penugasan induk atau tarif + if ($totalOngkosTugas <= 0) { + $totalOngkosTugas = $this->calculatePenugasanValue($penugasan); + } + + // Lewati jika ongkos masih 0 setelah semua fallback + if ($totalOngkosTugas <= 0) { + continue; + } + + // Bagian ongkos = total ongkos dibagi jumlah anggota tim yang hadir + $bagianOngkos = $totalOngkosTugas / $jumlahHadir; + $gaji_kotor += $bagianOngkos; + + $list_penugasan[] = [ + 'id_penugasan' => $penugasan->id_penugasan, + 'tanggal_selesai' => $penugasan->tanggal_diselesaikan ?? $penugasan->tanggal_diberikan, + 'lokasi' => $penugasan->alamat_lokasi + ?? $penugasan->lokasi_pekerjaan + ?? '-', + 'ongkos_penugasan' => $totalOngkosTugas, + 'jumlah_tim' => $jumlahHadir, + 'bagian_ongkos' => $bagianOngkos, + 'rincian_pekerjaan'=> $this->generateRincianLabel($penugasan), + ]; + } + + $biaya_makan = $jumlah_hari_kerja * 25000; + $total_kasbon = 0; + + if ($includeKasbon) { + $queryKasbon = Kasbon::where('id_teknisi', $idTeknisi) + ->where('status', 'belum_lunas'); + + if ($tanggalLimit) { + $queryKasbon->whereDate('tanggal_kasbon', '<=', $tanggalLimit); + } + + $total_kasbon = $queryKasbon->sum('jumlah_kasbon'); + } + + return [ + 'jumlah_hari_kerja' => $jumlah_hari_kerja, + 'gaji_kotor' => $gaji_kotor, + 'biaya_makan' => $biaya_makan, + 'total_kasbon' => $total_kasbon, + 'details' => $list_penugasan, + ]; + } + + /** + * Calculate hari kerja dari absensi + */ + private function calculateHariKerja($idTeknisi, $bulan, $tahun) + { + return Absensi::where('id_teknisi', $idTeknisi) + ->whereMonth('tanggal', $bulan) + ->whereYear('tanggal', $tahun) + ->where('status', 'hadir') + ->count(); + } + + /** + * Calculate nilai penugasan dengan fallback ke tarif + */ + private function calculatePenugasanValue($penugasan): float + { + if ($penugasan->total_nilai_pekerjaan > 0) { + return (float) $penugasan->total_nilai_pekerjaan; + } + + $tarif = $penugasan->tarif; + if (!$tarif) { + $query = TarifPekerjaan::where('jenis_pekerjaan', $penugasan->jenis_pekerjaan) + ->where('is_active', true); + if ($penugasan->dimensi_pipa) { + $query->where('dimensi_pipa', $penugasan->dimensi_pipa); + } + $tarif = $query->first(); + } + + if (!$tarif) return 0; + + if ($penugasan->jarak_meter > 0 && $tarif->tarif_per_meter) { + return (float) $tarif->tarif_per_meter * (float) $penugasan->jarak_meter; + } + if ($penugasan->jumlah_unit > 0 && $tarif->tarif_per_unit) { + return (float) $tarif->tarif_per_unit * (int) $penugasan->jumlah_unit; + } + if ($penugasan->jumlah_titik > 0 && $tarif->tarif_per_unit) { + return (float) $tarif->tarif_per_unit * (int) $penugasan->jumlah_titik; + } + + return (float) ($tarif->tarif_per_unit ?? $tarif->tarif_per_meter ?? 0); + } + + /** + * Generate label rincian pekerjaan untuk slip gaji + */ + private function generateRincianLabel($penugasan): string + { + $rincian = []; + + if ($penugasan->items && $penugasan->items->count() > 0) { + foreach ($penugasan->items as $item) { + $detail = $item->jenis_pekerjaan; + if ($item->jarak_meter > 0) { + $detail .= " ({$item->jarak_meter}m)"; + } elseif ($item->jumlah_unit > 0) { + $detail .= " ({$item->jumlah_unit} Unit)"; + } elseif ($item->jumlah_titik > 0) { + $detail .= " ({$item->jumlah_titik} Titik)"; + } + $rincian[] = $detail; + } + } else { + // Legacy: penugasan belum punya penugasan_items + if ($penugasan->jarak_meter > 0) { + $rincian[] = "{$penugasan->jarak_meter} Meter"; + } elseif ($penugasan->jumlah_unit > 0) { + $rincian[] = "{$penugasan->jumlah_unit} Unit"; + } else { + $rincian[] = "Borongan"; + } + } + + return implode(', ', $rincian); + } + + /** + * Calculate nilai untuk setiap PenugasanItem + */ + private function calculatePenugasanItemValue($item): float + { + if ($item->total_nilai_pekerjaan > 0) { + return (float) $item->total_nilai_pekerjaan; + } + + $tarif = $item->tarif; + if (!$tarif) { + $query = TarifPekerjaan::where('jenis_pekerjaan', $item->jenis_pekerjaan) + ->where('is_active', true); + if ($item->dimensi_pipa) { + $query->where('dimensi_pipa', $item->dimensi_pipa); + } + $tarif = $query->first(); + } + + if (!$tarif) return 0; + + if ($item->jarak_meter > 0 && $tarif->tarif_per_meter) { + return (float) $tarif->tarif_per_meter * (float) $item->jarak_meter; + } + if ($item->jumlah_unit > 0 && $tarif->tarif_per_unit) { + return (float) $tarif->tarif_per_unit * (int) $item->jumlah_unit; + } + if ($item->jumlah_titik > 0 && $tarif->tarif_per_unit) { + return (float) $tarif->tarif_per_unit * (int) $item->jumlah_titik; + } + + return (float) ($tarif->tarif_per_unit ?? $tarif->tarif_per_meter ?? 0); + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Http/Controllers/PenugasanController.php b/samooapk/laravel/app/Http/Controllers/PenugasanController.php new file mode 100644 index 0000000..7072eac --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/PenugasanController.php @@ -0,0 +1,406 @@ +filled('status')) { + if ($request->status === 'garansi_aktif') { + $query->garansiAktif(); + } else { + $query->where('status_pekerjaan', $request->status); + } + } + + if ($request->filled('jenis_pekerjaan')) { + $query->where('jenis_pekerjaan', $request->jenis_pekerjaan); + } + + if ($request->filled('search')) { + $search = $request->search; + $query->where(function($q) use ($search) { + $q->where('catatan_admin', 'LIKE', "%{$search}%") + ->orWhere('detail_pekerjaan', 'LIKE', "%{$search}%") + ->orWhereHas('teknisi', function($tq) use ($search) { + $tq->where('nama', 'LIKE', "%{$search}%"); + }); + }); + } + + if ($request->filled('id_teknisi')) { + $teknisiId = $request->id_teknisi; + $query->where(function($q) use ($teknisiId) { + $q->where('id_teknisi', $teknisiId) + ->orWhereHas('timTeknisi', function($tq) use ($teknisiId) { + $tq->where('id_teknisi', $teknisiId); + }); + }); + } + + if ($request->filled('start_date') && $request->filled('end_date')) { + $query->whereBetween('tanggal_diberikan', [$request->start_date, $request->end_date]); + } elseif ($request->filled('start_date')) { + $query->where('tanggal_diberikan', '>=', $request->start_date); + } elseif ($request->filled('end_date')) { + $query->where('tanggal_diberikan', '<=', $request->end_date); + } + + $penugasan = $query->orderBy('created_at', 'desc')->paginate(10); + + $totalPenugasan = Penugasan::count(); + $belumMulai = Penugasan::where('status_pekerjaan', 'belum_mulai')->count(); + $dalamProses = Penugasan::where('status_pekerjaan', 'dalam_proses')->count(); + $selesai = Penugasan::where('status_pekerjaan', 'selesai')->count(); + $dibatalkan = Penugasan::where('status_pekerjaan', 'dibatalkan')->count(); + $garansiAktif = Penugasan::garansiAktif()->count(); + + $today = Carbon::today()->toDateString(); + $absentTechIds = \App\Models\Absensi::where('tanggal', $today) + ->whereIn('status', ['izin', 'sakit']) + ->pluck('id_teknisi') + ->toArray(); + + $abandonedTasks = collect(); + if (!empty($absentTechIds)) { + $abandonedTasks = Penugasan::whereIn('status_pekerjaan', ['belum_mulai', 'dalam_proses']) + ->where(function($q) use ($absentTechIds) { + $q->whereIn('id_teknisi', $absentTechIds) + ->orWhereHas('timTeknisi', function($tq) use ($absentTechIds) { + $tq->whereIn('id_teknisi', $absentTechIds); + }); + }) + ->with('teknisi') + ->get(); + } + + $teknisis = Teknisi::where('status', 'aktif')->orderBy('nama')->get(); + $teknisiList = $teknisis; // Use same data for the new dropdown + + return view('Admin.KelolaPekerjaan.Penugasan', compact( + 'penugasan', 'totalPenugasan', 'belumMulai', 'dalamProses', + 'selesai', 'dibatalkan', 'garansiAktif', 'teknisis', 'teknisiList', 'abandonedTasks' + )); + } + + public function store(Request $request) + { + try { + $validated = $request->validate([ + 'id_teknisi' => 'required|array', + 'id_teknisi.*' => 'exists:teknisis,id_teknisi', + 'tanggal_diberikan' => 'required|date', + 'foto_surat' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:5120', + 'catatan_admin' => 'nullable|string', + 'jenis_pekerjaan' => 'required|string', + 'alamat_lokasi' => 'required|string', + 'nama_pelanggan' => 'nullable|string', + 'no_sambungan' => 'nullable|string', + ]); + + DB::beginTransaction(); + + $path = null; + if ($request->hasFile('foto_surat')) { + $path = $request->file('foto_surat')->store('penugasan/surat', 'public'); + } + + $penugasan = Penugasan::create([ + 'id_teknisi' => $validated['id_teknisi'][0], + 'foto_surat' => $path, + 'tanggal_diberikan' => $validated['tanggal_diberikan'], + 'catatan_admin' => $validated['catatan_admin'] ?? null, + 'jenis_pekerjaan' => $validated['jenis_pekerjaan'], + 'alamat_lokasi' => $validated['alamat_lokasi'], + 'nama_pelanggan' => $validated['nama_pelanggan'] ?? null, + 'no_sambungan' => $validated['no_sambungan'] ?? null, + 'status_pekerjaan' => 'belum_mulai', + ]); + + $techIds = array_unique($validated['id_teknisi']); + foreach ($techIds as $techId) { + TimTeknisiPenugasan::create([ + 'id_penugasan' => $penugasan->id_penugasan, + 'id_teknisi' => $techId, + 'status_kehadiran' => 'hadir', + ]); + } + + DB::commit(); + + return redirect()->route('pekerjaan.penugasan.index') + ->with('success', 'Penugasan berhasil dibuat! Teknisi akan melengkapi detail via mobile.'); + + } catch (\Exception $e) { + DB::rollBack(); + return redirect()->back() + ->with('error', 'Gagal menambahkan data: ' . $e->getMessage()) + ->withInput(); + } + } + + /** + * ✅ FIX: Tambah field garansi lengkap di response show() + * - is_garansi_aktif → true/false apakah garansi masih aktif + * - sisa_hari_garansi → berapa hari tersisa + * - tanggal_garansi_mulai → sudah ikut dari toArray() + * - tanggal_garansi_selesai → sudah ikut dari toArray() + * + * Sebelumnya field-field ini tidak di-append sehingga + * badge garansi di modal detail tidak pernah muncul + * meski datanya sudah ada di database + */ + public function show($id) + { + try { + $penugasan = Penugasan::with(['teknisi', 'tarif', 'timTeknisi.teknisi', 'items.tarif']) + ->findOrFail($id); + + $data = $penugasan->toArray(); + + $data['foto_surat_url'] = $penugasan->foto_surat_url; + $data['foto_sebelum_url'] = $penugasan->foto_sebelum_url; + $data['foto_sesudah_url'] = $penugasan->foto_sesudah_url; + $data['label_jenis_pekerjaan'] = $penugasan->label_jenis_pekerjaan; + + // ✅ FIX: Info garansi — dibutuhkan oleh blade untuk tampilkan badge + $data['is_garansi_aktif'] = $penugasan->isGaransiAktif(); + $data['sisa_hari_garansi'] = $penugasan->getSisaHariGaransi(); + + return response()->json(['success' => true, 'data' => $data]); + + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404); + } + } + + public function edit($id) + { + try { + $penugasan = Penugasan::with(['teknisi', 'timTeknisi.teknisi'])->findOrFail($id); + + $data = $penugasan->toArray(); + $data['foto_surat_url'] = $penugasan->foto_surat_url; + + return response()->json(['success' => true, 'data' => $data]); + + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404); + } + } + + public function update(Request $request, $id) + { + try { + $penugasan = Penugasan::findOrFail($id); + + $validated = $request->validate([ + 'id_teknisi' => 'required|array', + 'id_teknisi.*' => 'exists:teknisis,id_teknisi', + 'tanggal_diberikan' => 'required|date', + 'foto_surat' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:5120', + 'catatan_admin' => 'nullable|string', + 'jenis_pekerjaan' => 'required|string', + 'alamat_lokasi' => 'required|string', + 'nama_pelanggan' => 'nullable|string', + 'no_sambungan' => 'nullable|string', + ]); + + DB::beginTransaction(); + + $updateData = [ + 'id_teknisi' => $validated['id_teknisi'][0], + 'tanggal_diberikan' => $validated['tanggal_diberikan'], + 'catatan_admin' => $validated['catatan_admin'] ?? null, + 'jenis_pekerjaan' => $validated['jenis_pekerjaan'], + 'alamat_lokasi' => $validated['alamat_lokasi'], + 'nama_pelanggan' => $validated['nama_pelanggan'] ?? null, + 'no_sambungan' => $validated['no_sambungan'] ?? null, + ]; + + if ($request->hasFile('foto_surat')) { + if ($penugasan->foto_surat) { + Storage::disk('public')->delete($penugasan->foto_surat); + } + $updateData['foto_surat'] = $request->file('foto_surat')->store('penugasan/surat', 'public'); + } + + $penugasan->update($updateData); + + TimTeknisiPenugasan::where('id_penugasan', $penugasan->id_penugasan)->delete(); + + $techIds = array_unique($validated['id_teknisi']); + foreach ($techIds as $techId) { + TimTeknisiPenugasan::create([ + 'id_penugasan' => $penugasan->id_penugasan, + 'id_teknisi' => $techId, + 'status_kehadiran' => 'hadir', + ]); + } + + DB::commit(); + + return response()->json(['success' => true, 'message' => 'Data berhasil diupdate!']); + + } catch (\Exception $e) { + DB::rollBack(); + return response()->json(['success' => false, 'message' => 'Gagal update: ' . $e->getMessage()], 500); + } + } + + public function destroy($id) + { + try { + DB::beginTransaction(); + + $penugasan = Penugasan::findOrFail($id); + + if ($penugasan->foto_surat) { + Storage::disk('public')->delete($penugasan->foto_surat); + } + + $penugasan->delete(); + + DB::commit(); + + return response()->json(['success' => true, 'message' => 'Data berhasil dihapus!']); + + } catch (\Exception $e) { + DB::rollBack(); + return response()->json(['success' => false, 'message' => 'Gagal menghapus: ' . $e->getMessage()], 500); + } + } + + public function getTarifByJenis(Request $request) + { + try { + $tarifs = TarifPekerjaan::where('jenis_pekerjaan', $request->jenis_pekerjaan) + ->where('is_active', true) + ->get(); + + return response()->json(['success' => true, 'data' => $tarifs]); + + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => 'Gagal mengambil tarif'], 500); + } + } + + public function getTeknisiByDate(Request $request) + { + try { + // Selalu tampilkan seluruh teknisi aktif agar fleksibel secara operasional + $teknisis = Teknisi::where('status', 'aktif') + ->orderBy('nama') + ->get(); + + return response()->json(['success' => true, 'data' => $teknisis]); + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => 'Gagal mengambil data teknisi: ' . $e->getMessage()], 500); + } + } + + + /** + * ✅ FIX: Filter tanggal monitoring pakai tanggal_diselesaikan + * bukan updated_at agar hasil filter akurat + */ + public function monitoring(Request $request) + { + $query = Penugasan::with(['teknisi', 'timTeknisi.teknisi']); + + if ($request->filled('status')) { + $query->where('status_pekerjaan', $request->status); + } + + if ($request->filled('teknisi_id')) { + $teknisiId = $request->teknisi_id; + $query->where(function($q) use ($teknisiId) { + $q->where('id_teknisi', $teknisiId) + ->orWhereHas('timTeknisi', function($tq) use ($teknisiId) { + $tq->where('id_teknisi', $teknisiId); + }); + }); + } + + if ($request->filled('start_date') && $request->filled('end_date')) { + $start = $request->start_date . ' 00:00:00'; + $end = $request->end_date . ' 23:59:59'; + $query->where(function($q) use ($start, $end) { + $q->where(function($q2) use ($start, $end) { + $q2->whereNotNull('tanggal_diselesaikan') + ->whereBetween('tanggal_diselesaikan', [$start, $end]); + })->orWhere(function($q2) use ($start, $end) { + $q2->whereNull('tanggal_diselesaikan') + ->whereBetween('updated_at', [$start, $end]); + }); + }); + } elseif ($request->filled('start_date')) { + $start = $request->start_date . ' 00:00:00'; + $query->where(function($q) use ($start) { + $q->where(function($q2) use ($start) { + $q2->whereNotNull('tanggal_diselesaikan') + ->where('tanggal_diselesaikan', '>=', $start); + })->orWhere(function($q2) use ($start) { + $q2->whereNull('tanggal_diselesaikan') + ->where('updated_at', '>=', $start); + }); + }); + } elseif ($request->filled('end_date')) { + $end = $request->end_date . ' 23:59:59'; + $query->where(function($q) use ($end) { + $q->where(function($q2) use ($end) { + $q2->whereNotNull('tanggal_diselesaikan') + ->where('tanggal_diselesaikan', '<=', $end); + })->orWhere(function($q2) use ($end) { + $q2->whereNull('tanggal_diselesaikan') + ->where('updated_at', '<=', $end); + }); + }); + } + + $progresKerja = $query->orderBy('updated_at', 'desc')->paginate(12); + + $progresKerja->getCollection()->transform(function ($item) { + $item->persentase_pekerjaan = match($item->status_pekerjaan) { + 'selesai' => 100, + 'dalam_proses' => 50, + default => 0, + }; + $item->is_garansi_aktif = $item->isGaransiAktif(); + $item->sisa_hari_garansi = $item->getSisaHariGaransi(); + return $item; + }); + + $statistics = [ + 'total' => Penugasan::count(), + 'dalam_progres' => Penugasan::where('status_pekerjaan', 'dalam_proses')->count(), + 'selesai' => Penugasan::where('status_pekerjaan', 'selesai')->count(), + 'belum_mulai' => Penugasan::where('status_pekerjaan', 'belum_mulai')->count(), + 'garansi_aktif' => Penugasan::garansiAktif()->count(), + ]; + + $statusList = [ + 'belum_mulai' => 'Belum Mulai', + 'dalam_proses' => 'Dalam Proses', + 'selesai' => 'Selesai', + ]; + + $teknisiList = Teknisi::orderBy('nama')->get(); + + return view('Admin.KelolaPekerjaan.ProgresKerja', compact( + 'progresKerja', 'statistics', 'statusList', 'teknisiList' + )); + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Http/Controllers/ProfileController.php b/samooapk/laravel/app/Http/Controllers/ProfileController.php new file mode 100644 index 0000000..a48eb8d --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/ProfileController.php @@ -0,0 +1,60 @@ + $request->user(), + ]); + } + + /** + * Update the user's profile information. + */ + public function update(ProfileUpdateRequest $request): RedirectResponse + { + $request->user()->fill($request->validated()); + + if ($request->user()->isDirty('email')) { + $request->user()->email_verified_at = null; + } + + $request->user()->save(); + + return Redirect::route('profile.edit')->with('status', 'profile-updated'); + } + + /** + * Delete the user's account. + */ + public function destroy(Request $request): RedirectResponse + { + $request->validateWithBag('userDeletion', [ + 'password' => ['required', 'current_password'], + ]); + + $user = $request->user(); + + Auth::logout(); + + $user->delete(); + + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return Redirect::to('/'); + } +} diff --git a/samooapk/laravel/app/Http/Controllers/ProgresKerjaController.php b/samooapk/laravel/app/Http/Controllers/ProgresKerjaController.php new file mode 100644 index 0000000..dc136e0 --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/ProgresKerjaController.php @@ -0,0 +1,464 @@ +filled('status')) { + $query->byStatus($request->status); + } + + // Filter by teknisi + if ($request->filled('teknisi_id')) { + $query->byTeknisi($request->teknisi_id); + } + + // Filter by date range + if ($request->filled('start_date') || $request->filled('end_date')) { + $query->byDateRange($request->start_date, $request->end_date); + } + + // Search functionality + if ($request->filled('search')) { + $search = $request->search; + $query->where(function($q) use ($search) { + $q->where('tugas', 'like', "%{$search}%") + ->orWhere('deskripsi_progres', 'like', "%{$search}%") + ->orWhereHas('teknisi', function($teknisiQuery) use ($search) { + $teknisiQuery->where('nama', 'like', "%{$search}%"); + }); + }); + } + + $progresKerja = $query->latest()->paginate(10); + + // Get filter options + $teknisiList = Teknisi::orderBy('nama')->get(); + $statusList = ProgresKerja::$statusProgres; + + // Get statistics + $statistics = ProgresKerja::getStatistics(); + + return view('Admin.KelolaPekerjaan.ProgresKerja', compact( + 'progresKerja', + 'teknisiList', + 'statusList', + 'statistics' + )); + } + + /** + * Show the form for creating a new progress kerja. + * + * @return \Illuminate\Http\Response + */ + public function create() + { + $penugasanList = Penugasan::with('pelanggan')->orderBy('created_at', 'desc')->get(); + $teknisiList = Teknisi::orderBy('nama')->get(); + $statusList = ProgresKerja::$statusProgres; + + return view('Admin.KelolaPekerjaan.ProgresKerjaForm', compact( + 'penugasanList', + 'teknisiList', + 'statusList' + )); + } + + /** + * Store a newly created progress kerja in storage. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + */ + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'id_penugasan' => 'required|exists:penugasan,id_penugasan', + 'id_teknisi' => 'required|exists:teknisi,id_teknisi', + 'tugas' => 'required|string|max:255', + 'deskripsi_progres' => 'nullable|string', + 'status_progres' => 'required|in:' . implode(',', array_keys(ProgresKerja::$statusProgres)), + 'persentase_pekerjaan' => 'required|integer|min:0|max:100', + 'foto_progress' => 'nullable|image|mimes:jpeg,png,jpg|max:2048', + ]); + + if ($validator->fails()) { + return redirect()->back() + ->withErrors($validator) + ->withInput(); + } + + $data = $request->only([ + 'id_penugasan', + 'id_teknisi', + 'tugas', + 'deskripsi_progres', + 'status_progres', + 'persentase_pekerjaan' + ]); + + // Handle foto progress upload + if ($request->hasFile('foto_progress')) { + $file = $request->file('foto_progress'); + $fileName = time() . '_' . Str::random(10) . '.' . $file->getClientOriginalExtension(); + + // Store in public/storage/progress_photos + $file->storeAs('progress_photos', $fileName, 'public'); + $data['foto_progress'] = $fileName; + } + + $progresKerja = ProgresKerja::create($data); + + // Auto update status based on percentage + $progresKerja->autoUpdateStatus(); + + return redirect()->route('progres-kerja.index') + ->with('success', 'Progress kerja berhasil ditambahkan.'); + } + + /** + * Display the specified progress kerja. + * + * @param \App\Models\ProgresKerja $progresKerja + * @return \Illuminate\Http\Response + */ + public function show(ProgresKerja $progresKerja) + { + $progresKerja->load(['teknisi', 'penugasan.pelanggan']); + + return view('Admin.KelolaPekerjaan.ProgresKerjaDetail', compact('progresKerja')); + } + + /** + * Show the form for editing the specified progress kerja. + * + * @param \App\Models\ProgresKerja $progresKerja + * @return \Illuminate\Http\Response + */ + public function edit(ProgresKerja $progresKerja) + { + $penugasanList = Penugasan::with('pelanggan')->orderBy('created_at', 'desc')->get(); + $teknisiList = Teknisi::orderBy('nama')->get(); + $statusList = ProgresKerja::$statusProgres; + + return view('Admin.KelolaPekerjaan.ProgresKerjaForm', compact( + 'progresKerja', + 'penugasanList', + 'teknisiList', + 'statusList' + )); + } + + /** + * Update the specified progress kerja in storage. + * + * @param \Illuminate\Http\Request $request + * @param \App\Models\ProgresKerja $progresKerja + * @return \Illuminate\Http\Response + */ + public function update(Request $request, ProgresKerja $progresKerja) + { + $validator = Validator::make($request->all(), [ + 'id_penugasan' => 'required|exists:penugasan,id_penugasan', + 'id_teknisi' => 'required|exists:teknisi,id_teknisi', + 'tugas' => 'required|string|max:255', + 'deskripsi_progres' => 'nullable|string', + 'status_progres' => 'required|in:' . implode(',', array_keys(ProgresKerja::$statusProgres)), + 'persentase_pekerjaan' => 'required|integer|min:0|max:100', + 'foto_progress' => 'nullable|image|mimes:jpeg,png,jpg|max:2048', + ]); + + if ($validator->fails()) { + return redirect()->back() + ->withErrors($validator) + ->withInput(); + } + + $data = $request->only([ + 'id_penugasan', + 'id_teknisi', + 'tugas', + 'deskripsi_progres', + 'status_progres', + 'persentase_pekerjaan' + ]); + + // Handle foto progress upload + if ($request->hasFile('foto_progress')) { + // Delete old photo if exists + if ($progresKerja->foto_progress) { + Storage::disk('public')->delete('progress_photos/' . $progresKerja->foto_progress); + } + + $file = $request->file('foto_progress'); + $fileName = time() . '_' . Str::random(10) . '.' . $file->getClientOriginalExtension(); + + $file->storeAs('progress_photos', $fileName, 'public'); + $data['foto_progress'] = $fileName; + } + + $progresKerja->update($data); + + // Auto update status based on percentage + $progresKerja->autoUpdateStatus(); + + return redirect()->route('progres-kerja.index') + ->with('success', 'Progress kerja berhasil diperbarui.'); + } + + /** + * Remove the specified progress kerja from storage. + * + * @param \App\Models\ProgresKerja $progresKerja + * @return \Illuminate\Http\Response + */ + public function destroy(ProgresKerja $progresKerja) + { + // Delete photo if exists + if ($progresKerja->foto_progress) { + Storage::disk('public')->delete('progress_photos/' . $progresKerja->foto_progress); + } + + $progresKerja->delete(); + + return redirect()->route('progres-kerja.index') + ->with('success', 'Progress kerja berhasil dihapus.'); + } + + /** + * Quick update progress status + * + * @param \Illuminate\Http\Request $request + * @param \App\Models\ProgresKerja $progresKerja + * @return \Illuminate\Http\JsonResponse + */ + public function updateStatus(Request $request, ProgresKerja $progresKerja) + { + $validator = Validator::make($request->all(), [ + 'status' => 'required|in:' . implode(',', array_keys(ProgresKerja::$statusProgres)), + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Status tidak valid.' + ], 400); + } + + // Check if transition is valid + if (!$progresKerja->canTransitionTo($request->status)) { + return response()->json([ + 'success' => false, + 'message' => 'Transisi status tidak valid.' + ], 400); + } + + $progresKerja->update(['status_progres' => $request->status]); + + return response()->json([ + 'success' => true, + 'message' => 'Status berhasil diperbarui.', + 'new_status' => $progresKerja->status_formatted, + 'badge_class' => $progresKerja->status_badge_class + ]); + } + + /** + * Update progress percentage + * + * @param \Illuminate\Http\Request $request + * @param \App\Models\ProgresKerja $progresKerja + * @return \Illuminate\Http\JsonResponse + */ + public function updatePercentage(Request $request, ProgresKerja $progresKerja) + { + $validator = Validator::make($request->all(), [ + 'percentage' => 'required|integer|min:0|max:100', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Persentase tidak valid.' + ], 400); + } + + $progresKerja->update(['persentase_pekerjaan' => $request->percentage]); + $progresKerja->autoUpdateStatus(); + + return response()->json([ + 'success' => true, + 'message' => 'Persentase berhasil diperbarui.', + 'new_percentage' => $progresKerja->persentase_pekerjaan, + 'new_status' => $progresKerja->status_formatted, + 'badge_class' => $progresKerja->status_badge_class + ]); + } + + /** + * Get progress by teknisi (AJAX) + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function getByTeknisi(Request $request) + { + $teknisiId = $request->get('teknisi_id'); + + if (!$teknisiId) { + return response()->json([ + 'success' => false, + 'message' => 'Teknisi ID diperlukan.' + ], 400); + } + + $progresKerja = ProgresKerja::getByTeknisi($teknisiId); + + return response()->json([ + 'success' => true, + 'data' => $progresKerja->map(function($item) { + return [ + 'id' => $item->id_progres, + 'tugas' => $item->tugas, + 'status' => $item->status_formatted, + 'persentase' => $item->persentase_pekerjaan, + 'tanggal_update' => $item->tanggal_update_formatted, + 'penugasan' => $item->penugasan->nama_pekerjaan ?? 'N/A' + ]; + }) + ]); + } + + /** + * Export progress data + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + */ + public function export(Request $request) + { + $query = ProgresKerja::with(['teknisi', 'penugasan']); + + // Apply same filters as index + if ($request->filled('status')) { + $query->byStatus($request->status); + } + + if ($request->filled('teknisi_id')) { + $query->byTeknisi($request->teknisi_id); + } + + if ($request->filled('start_date') || $request->filled('end_date')) { + $query->byDateRange($request->start_date, $request->end_date); + } + + $progresKerja = $query->latest()->get(); + + $filename = 'progress_kerja_' . date('Y-m-d') . '.csv'; + + $headers = [ + 'Content-Type' => 'text/csv', + 'Content-Disposition' => 'attachment; filename="' . $filename . '"', + ]; + + $callback = function() use ($progresKerja) { + $file = fopen('php://output', 'w'); + + // Add BOM for UTF-8 + fwrite($file, "\xEF\xBB\xBF"); + + // Header row + fputcsv($file, [ + 'ID', + 'Penugasan', + 'Teknisi', + 'Tugas', + 'Deskripsi Progress', + 'Status', + 'Persentase (%)', + 'Tanggal Update', + 'Foto Progress' + ]); + + // Data rows + foreach ($progresKerja as $item) { + fputcsv($file, [ + $item->id_progres, + $item->penugasan->nama_pekerjaan ?? 'N/A', + $item->teknisi->nama ?? 'N/A', + $item->tugas, + $item->deskripsi_progres, + $item->status_formatted, + $item->persentase_pekerjaan, + $item->tanggal_update_formatted, + $item->has_foto ? 'Ya' : 'Tidak' + ]); + } + + fclose($file); + }; + + return response()->stream($callback, 200, $headers); + } + + /** + * Get recent updates for dashboard + * + * @return \Illuminate\Http\JsonResponse + */ + public function getRecentUpdates() + { + $recentUpdates = ProgresKerja::getRecentUpdates(5); + + return response()->json([ + 'success' => true, + 'data' => $recentUpdates->map(function($item) { + return [ + 'id' => $item->id_progres, + 'tugas' => $item->tugas, + 'teknisi' => $item->teknisi->nama ?? 'N/A', + 'status' => $item->status_formatted, + 'persentase' => $item->persentase_pekerjaan, + 'tanggal_update' => $item->tanggal_update_formatted, + 'days_since_update' => $item->days_since_update + ]; + }) + ]); + } + + /** + * Get statistics for dashboard + * + * @return \Illuminate\Http\JsonResponse + */ + public function getStatistics() + { + $statistics = ProgresKerja::getStatistics(); + + return response()->json([ + 'success' => true, + 'data' => $statistics + ]); + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Http/Controllers/TeknisiController.php b/samooapk/laravel/app/Http/Controllers/TeknisiController.php new file mode 100644 index 0000000..80cd89e --- /dev/null +++ b/samooapk/laravel/app/Http/Controllers/TeknisiController.php @@ -0,0 +1,308 @@ +has('q') && $request->q != '') { + $searchTerm = $request->q; + $query->where(function($q) use ($searchTerm) { + $q->where('nama', 'LIKE', "%{$searchTerm}%") + ->orWhere('email', 'LIKE', "%{$searchTerm}%") + ->orWhere('no_telephone', 'LIKE', "%{$searchTerm}%"); + }); + } + + $teknisis = $query->latest()->get(); + + // Jika request dari AJAX, return JSON + if ($request->ajax() || $request->wantsJson()) { + return response()->json([ + 'success' => true, + 'data' => $teknisis + ]); + } + + // Jika request biasa, return view + return view('Admin.KelolaTeknisi.Teknisi', compact('teknisis')); + } + + /** + * Show the form for creating a new resource. + */ + public function create() + { + return view('teknisi.create'); + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'nama' => 'required|string|max:100', + 'tanggal_lahir' => 'required|date', + 'alamat' => 'required|string', + 'email' => 'nullable|email|max:100|unique:teknisis,email', + 'no_telephone' => 'required|string|max:15', + 'tanggal_masuk' => 'required|date', + 'status' => 'required|in:aktif,tidak_aktif', + ], [ + 'nama.required' => 'Nama wajib diisi', + 'nama.max' => 'Nama maksimal 100 karakter', + 'tanggal_lahir.required' => 'Tanggal lahir wajib diisi', + 'tanggal_lahir.date' => 'Format tanggal lahir tidak valid', + 'alamat.required' => 'Alamat wajib diisi', + 'email.email' => 'Format email tidak valid', + 'email.unique' => 'Email sudah terdaftar', + 'no_telephone.required' => 'Nomor telephone wajib diisi', + 'no_telephone.max' => 'Nomor telephone maksimal 15 karakter', + 'tanggal_masuk.required' => 'Tanggal masuk wajib diisi', + 'tanggal_masuk.date' => 'Format tanggal masuk tidak valid', + 'status.required' => 'Status wajib dipilih', + 'status.in' => 'Status harus aktif atau tidak_aktif', + ]); + + if ($validator->fails()) { + // Jika AJAX request + if (request()->ajax() || request()->wantsJson()) { + return response()->json([ + 'success' => false, + 'errors' => $validator->errors() + ], 422); + } + + return redirect()->back() + ->withErrors($validator) + ->withInput(); + } + + try { + $teknisi = Teknisi::create($request->all()); + + // Jika AJAX request + if (request()->ajax() || request()->wantsJson()) { + return response()->json([ + 'success' => true, + 'message' => 'Data teknisi berhasil ditambahkan', + 'data' => $teknisi + ], 201); + } + + return redirect()->route('teknisi.index') + ->with('success', 'Data teknisi berhasil ditambahkan'); + } catch (\Exception $e) { + Log::error('Error creating teknisi: ' . $e->getMessage()); + + // Jika AJAX request + if (request()->ajax() || request()->wantsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal menambahkan data teknisi: ' . $e->getMessage() + ], 500); + } + + return redirect()->back() + ->with('error', 'Gagal menambahkan data teknisi: ' . $e->getMessage()) + ->withInput(); + } + } + + /** + * Display the specified resource. + */ + public function show(string $id) + { + try { + $teknisi = Teknisi::where('id_teknisi', $id)->firstOrFail(); + + // Jika AJAX request, return JSON + if (request()->ajax() || request()->wantsJson()) { + return response()->json([ + 'success' => true, + 'data' => $teknisi + ]); + } + + return view('teknisi.show', compact('teknisi')); + } catch (\Exception $e) { + if (request()->ajax() || request()->wantsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Data teknisi tidak ditemukan' + ], 404); + } + + return redirect()->route('teknisi.index') + ->with('error', 'Data teknisi tidak ditemukan'); + } + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(string $id) + { + try { + $teknisi = Teknisi::where('id_teknisi', $id)->firstOrFail(); + return view('teknisi.edit', compact('teknisi')); + } catch (\Exception $e) { + return redirect()->route('teknisi.index') + ->with('error', 'Data teknisi tidak ditemukan'); + } + } + + /** + * Update the specified resource in storage. + */ + + public function update(Request $request, string $id) +{ + try { + $teknisi = Teknisi::where('id_teknisi', $id)->firstOrFail(); + + // Log untuk debugging + Log::info('Update Teknisi Request', [ + 'id' => $id, + 'request_data' => $request->all() + ]); + + // Validasi - TANPA tanggal_lahir dan tanggal_masuk karena tidak boleh diubah + $validator = Validator::make($request->all(), [ + 'nama' => 'required|string|max:100', + 'alamat' => 'required|string', + 'email' => 'nullable|email|max:100|unique:teknisis,email,' . $id . ',id_teknisi', + 'no_telephone' => 'required|string|max:15', + 'status' => 'required|in:aktif,tidak_aktif', + 'tanggal_masuk' => 'required|date', + ], [ + 'nama.required' => 'Nama wajib diisi', + 'nama.max' => 'Nama maksimal 100 karakter', + 'alamat.required' => 'Alamat wajib diisi', + 'email.email' => 'Format email tidak valid', + 'email.unique' => 'Email sudah terdaftar', + 'no_telephone.required' => 'Nomor telephone wajib diisi', + 'no_telephone.max' => 'Nomor telephone maksimal 15 karakter', + 'status.required' => 'Status wajib dipilih', + 'status.in' => 'Status harus aktif atau tidak_aktif', + 'tanggal_masuk.required' => 'Tanggal masuk wajib diisi', + 'tanggal_masuk.date' => 'Format tanggal masuk tidak valid', + ]); + + if ($validator->fails()) { + Log::warning('Validation Failed', [ + 'errors' => $validator->errors()->toArray() + ]); + + // Jika AJAX request + if (request()->ajax() || request()->wantsJson()) { + return response()->json([ + 'success' => false, + 'errors' => $validator->errors() + ], 422); + } + + return redirect()->back() + ->withErrors($validator) + ->withInput(); + } + + // Update data + $teknisi->update([ + 'nama' => $request->nama, + 'alamat' => $request->alamat, + 'email' => $request->email, + 'no_telephone' => $request->no_telephone, + 'status' => $request->status, + 'tanggal_masuk' => $request->tanggal_masuk, + ]); + + Log::info('Teknisi Updated Successfully', [ + 'id' => $id, + 'updated_data' => $teknisi->fresh()->toArray() + ]); + + // Jika AJAX request + if (request()->ajax() || request()->wantsJson()) { + return response()->json([ + 'success' => true, + 'message' => 'Data teknisi berhasil diperbarui', + 'data' => $teknisi + ]); + } + + return redirect()->route('teknisi.index') + ->with('success', 'Data teknisi berhasil diperbarui'); + + } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { + Log::error('Teknisi Not Found', ['id' => $id]); + + // Jika AJAX request + if (request()->ajax() || request()->wantsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Data teknisi tidak ditemukan' + ], 404); + } + + return redirect()->route('teknisi.index') + ->with('error', 'Data teknisi tidak ditemukan'); + + } catch (\Exception $e) { + Log::error('Update Teknisi Failed', [ + 'id' => $id, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + // Jika AJAX request + if (request()->ajax() || request()->wantsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal memperbarui data teknisi: ' . $e->getMessage() + ], 500); + } + + return redirect()->back() + ->with('error', 'Gagal memperbarui data teknisi: ' . $e->getMessage()) + ->withInput(); + } +} + /** + * Search teknisi + */ + public function search(Request $request) + { + $query = $request->get('q'); + + $teknisis = Teknisi::where('nama', 'LIKE', "%{$query}%") + ->orWhere('email', 'LIKE', "%{$query}%") + ->orWhere('no_telephone', 'LIKE', "%{$query}%") + ->latest() + ->get(); + + // Jika AJAX request + if (request()->ajax() || request()->wantsJson()) { + return response()->json([ + 'success' => true, + 'data' => $teknisis + ]); + } + + return view('Admin.KelolaTeknisi.Teknisi', compact('teknisis')); + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Http/Kernel.php b/samooapk/laravel/app/Http/Kernel.php new file mode 100644 index 0000000..494c050 --- /dev/null +++ b/samooapk/laravel/app/Http/Kernel.php @@ -0,0 +1,68 @@ + + */ + protected $middleware = [ + // \App\Http\Middleware\TrustHosts::class, + \App\Http\Middleware\TrustProxies::class, + \Illuminate\Http\Middleware\HandleCors::class, + \App\Http\Middleware\PreventRequestsDuringMaintenance::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 array> + */ + protected $middlewareGroups = [ + 'web' => [ + \App\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\VerifyCsrfToken::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ], + + 'api' => [ + // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, + \Illuminate\Routing\Middleware\ThrottleRequests::class.':api', + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ], + ]; + + /** + * The application's middleware aliases. + * + * Aliases may be used instead of class names to conveniently assign middleware to routes and groups. + * + * @var array + */ + protected $middlewareAliases = [ + 'auth' => \App\Http\Middleware\Authenticate::class, + 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, + 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, + 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, + 'can' => \Illuminate\Auth\Middleware\Authorize::class, + 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, + 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, + 'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class, + 'signed' => \App\Http\Middleware\ValidateSignature::class, + 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, + 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + ]; +} diff --git a/samooapk/laravel/app/Http/Middleware/Authenticate.php b/samooapk/laravel/app/Http/Middleware/Authenticate.php new file mode 100644 index 0000000..d4ef644 --- /dev/null +++ b/samooapk/laravel/app/Http/Middleware/Authenticate.php @@ -0,0 +1,17 @@ +expectsJson() ? null : route('login'); + } +} diff --git a/samooapk/laravel/app/Http/Middleware/EncryptCookies.php b/samooapk/laravel/app/Http/Middleware/EncryptCookies.php new file mode 100644 index 0000000..867695b --- /dev/null +++ b/samooapk/laravel/app/Http/Middleware/EncryptCookies.php @@ -0,0 +1,17 @@ + + */ + protected $except = [ + // + ]; +} diff --git a/samooapk/laravel/app/Http/Middleware/PreventRequestsDuringMaintenance.php b/samooapk/laravel/app/Http/Middleware/PreventRequestsDuringMaintenance.php new file mode 100644 index 0000000..74cbd9a --- /dev/null +++ b/samooapk/laravel/app/Http/Middleware/PreventRequestsDuringMaintenance.php @@ -0,0 +1,17 @@ + + */ + protected $except = [ + // + ]; +} diff --git a/samooapk/laravel/app/Http/Middleware/RedirectIfAuthenticated.php b/samooapk/laravel/app/Http/Middleware/RedirectIfAuthenticated.php new file mode 100644 index 0000000..afc78c4 --- /dev/null +++ b/samooapk/laravel/app/Http/Middleware/RedirectIfAuthenticated.php @@ -0,0 +1,30 @@ +check()) { + return redirect(RouteServiceProvider::HOME); + } + } + + return $next($request); + } +} diff --git a/samooapk/laravel/app/Http/Middleware/TrimStrings.php b/samooapk/laravel/app/Http/Middleware/TrimStrings.php new file mode 100644 index 0000000..88cadca --- /dev/null +++ b/samooapk/laravel/app/Http/Middleware/TrimStrings.php @@ -0,0 +1,19 @@ + + */ + protected $except = [ + 'current_password', + 'password', + 'password_confirmation', + ]; +} diff --git a/samooapk/laravel/app/Http/Middleware/TrustHosts.php b/samooapk/laravel/app/Http/Middleware/TrustHosts.php new file mode 100644 index 0000000..c9c58bd --- /dev/null +++ b/samooapk/laravel/app/Http/Middleware/TrustHosts.php @@ -0,0 +1,20 @@ + + */ + public function hosts(): array + { + return [ + $this->allSubdomainsOfApplicationUrl(), + ]; + } +} diff --git a/samooapk/laravel/app/Http/Middleware/TrustProxies.php b/samooapk/laravel/app/Http/Middleware/TrustProxies.php new file mode 100644 index 0000000..3391630 --- /dev/null +++ b/samooapk/laravel/app/Http/Middleware/TrustProxies.php @@ -0,0 +1,28 @@ +|string|null + */ + protected $proxies; + + /** + * The headers that should be used to detect proxies. + * + * @var int + */ + protected $headers = + Request::HEADER_X_FORWARDED_FOR | + Request::HEADER_X_FORWARDED_HOST | + Request::HEADER_X_FORWARDED_PORT | + Request::HEADER_X_FORWARDED_PROTO | + Request::HEADER_X_FORWARDED_AWS_ELB; +} diff --git a/samooapk/laravel/app/Http/Middleware/ValidateSignature.php b/samooapk/laravel/app/Http/Middleware/ValidateSignature.php new file mode 100644 index 0000000..093bf64 --- /dev/null +++ b/samooapk/laravel/app/Http/Middleware/ValidateSignature.php @@ -0,0 +1,22 @@ + + */ + protected $except = [ + // 'fbclid', + // 'utm_campaign', + // 'utm_content', + // 'utm_medium', + // 'utm_source', + // 'utm_term', + ]; +} diff --git a/samooapk/laravel/app/Http/Middleware/VerifyCsrfToken.php b/samooapk/laravel/app/Http/Middleware/VerifyCsrfToken.php new file mode 100644 index 0000000..9e86521 --- /dev/null +++ b/samooapk/laravel/app/Http/Middleware/VerifyCsrfToken.php @@ -0,0 +1,17 @@ + + */ + protected $except = [ + // + ]; +} diff --git a/samooapk/laravel/app/Http/Requests/Auth/LoginRequest.php b/samooapk/laravel/app/Http/Requests/Auth/LoginRequest.php new file mode 100644 index 0000000..2b92f65 --- /dev/null +++ b/samooapk/laravel/app/Http/Requests/Auth/LoginRequest.php @@ -0,0 +1,85 @@ + + */ + public function rules(): array + { + return [ + 'email' => ['required', 'string', 'email'], + 'password' => ['required', 'string'], + ]; + } + + /** + * Attempt to authenticate the request's credentials. + * + * @throws \Illuminate\Validation\ValidationException + */ + public function authenticate(): void + { + $this->ensureIsNotRateLimited(); + + if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { + RateLimiter::hit($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => trans('auth.failed'), + ]); + } + + RateLimiter::clear($this->throttleKey()); + } + + /** + * Ensure the login request is not rate limited. + * + * @throws \Illuminate\Validation\ValidationException + */ + public function ensureIsNotRateLimited(): void + { + if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { + return; + } + + event(new Lockout($this)); + + $seconds = RateLimiter::availableIn($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => trans('auth.throttle', [ + 'seconds' => $seconds, + 'minutes' => ceil($seconds / 60), + ]), + ]); + } + + /** + * Get the rate limiting throttle key for the request. + */ + public function throttleKey(): string + { + return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip()); + } +} diff --git a/samooapk/laravel/app/Http/Requests/ProfileUpdateRequest.php b/samooapk/laravel/app/Http/Requests/ProfileUpdateRequest.php new file mode 100644 index 0000000..93b0022 --- /dev/null +++ b/samooapk/laravel/app/Http/Requests/ProfileUpdateRequest.php @@ -0,0 +1,23 @@ + + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', Rule::unique(User::class)->ignore($this->user()->id)], + ]; + } +} diff --git a/samooapk/laravel/app/Models/Absensi.php b/samooapk/laravel/app/Models/Absensi.php new file mode 100644 index 0000000..68c5d2c --- /dev/null +++ b/samooapk/laravel/app/Models/Absensi.php @@ -0,0 +1,341 @@ + 'date', + 'jam_masuk' => 'datetime', + 'jam_keluar' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime' + ]; + + /** + * Status absensi yang valid + */ + const STATUS_HADIR = 'hadir'; + const STATUS_SAKIT = 'sakit'; + const STATUS_IZIN = 'izin'; + + /** + * Array status yang tersedia + */ + public static function getStatusOptions() + { + return [ + self::STATUS_HADIR => 'Hadir', + self::STATUS_SAKIT => 'Sakit', + self::STATUS_IZIN => 'Izin' + ]; + } + + /** + * Relasi ke model Teknisi + * Satu absensi dimiliki oleh satu teknisi + */ + public function teknisi() + { + return $this->belongsTo(Teknisi::class, 'id_teknisi', 'id_teknisi'); + } + + /** + * Scope untuk filter berdasarkan tanggal + */ + public function scopeFilterByDate($query, $tanggal) + { + if ($tanggal) { + return $query->whereDate('tanggal', $tanggal); + } + return $query; + } + + /** + * Scope untuk filter berdasarkan status + */ + public function scopeFilterByStatus($query, $status) + { + if ($status) { + return $query->where('status', $status); + } + return $query; + } + + /** + * Scope untuk filter berdasarkan teknisi + */ + public function scopeFilterByTeknisi($query, $teknisiId) + { + if ($teknisiId) { + return $query->where('id_teknisi', $teknisiId); + } + return $query; + } + + /** + * Scope untuk filter berdasarkan bulan dan tahun + */ + public function scopeFilterByMonth($query, $bulan, $tahun = null) + { + if (!$tahun) { + $tahun = date('Y'); + } + + return $query->whereMonth('tanggal', $bulan) + ->whereYear('tanggal', $tahun); + } + + /** + * Scope untuk data absensi hari ini + */ + public function scopeToday($query) + { + return $query->whereDate('tanggal', Carbon::today()); + } + + /** + * Scope untuk data absensi minggu ini + */ + public function scopeThisWeek($query) + { + return $query->whereBetween('tanggal', [ + Carbon::now()->startOfWeek(), + Carbon::now()->endOfWeek() + ]); + } + + /** + * Scope untuk data absensi bulan ini + */ + public function scopeThisMonth($query) + { + return $query->whereMonth('tanggal', Carbon::now()->month) + ->whereYear('tanggal', Carbon::now()->year); + } + + /** + * Accessor untuk format tanggal Indonesia + */ + public function getTanggalFormattedAttribute() + { + return Carbon::parse($this->tanggal)->format('d/m/Y'); + } + + /** + * Accessor untuk format jam masuk + */ + public function getJamMasukFormattedAttribute() + { + return $this->jam_masuk ? Carbon::parse($this->jam_masuk)->format('H:i') : '-'; + } + + /** + * Accessor untuk format jam keluar + */ + public function getJamKeluarFormattedAttribute() + { + return $this->jam_keluar ? Carbon::parse($this->jam_keluar)->format('H:i') : '-'; + } + + /** + * Accessor untuk nama status dengan format title case + */ + public function getStatusFormattedAttribute() + { + return ucfirst($this->status); + } + + /** + * Accessor untuk URL foto absen masuk + */ + public function getFotoAbsenMasukUrlAttribute() + { + return $this->foto_absen_masuk ? asset('storage/' . $this->foto_absen_masuk) : null; + } + + /** + * Accessor untuk URL foto absen keluar + */ + public function getFotoAbsenKeluarUrlAttribute() + { + return $this->foto_absen_keluar ? asset('storage/' . $this->foto_absen_keluar) : null; + } + + /** + * Accessor untuk menghitung durasi kerja (dalam menit) + */ + public function getDurasiKerjaAttribute() + { + if ($this->jam_masuk && $this->jam_keluar) { + $masuk = Carbon::parse($this->jam_masuk); + $keluar = Carbon::parse($this->jam_keluar); + return $keluar->diffInMinutes($masuk); + } + return 0; + } + + /** + * Accessor untuk durasi kerja dalam format jam:menit + */ + public function getDurasiKerjaFormattedAttribute() + { + $durasi = $this->durasi_kerja; + if ($durasi > 0) { + $jam = floor($durasi / 60); + $menit = $durasi % 60; + return sprintf('%02d:%02d', $jam, $menit); + } + return '00:00'; + } + + /** + * Accessor untuk menentukan apakah terlambat (asumsi jam masuk normal 08:00) + */ + /** + * Accessor untuk kategori kerja (Kerja Biasa vs Lembur) + */ + public function getKategoriKerjaAttribute() + { + if ($this->jam_masuk) { + $jamMasuk = Carbon::parse($this->jam_masuk); + $start = Carbon::parse('07:00'); + $end = Carbon::parse('18:00'); + + if ($jamMasuk->between($start, $end)) { + return 'Kerja Biasa'; + } + return 'Kerja Urgent'; + } + return '-'; + } + + /** + * Label warna untuk kategori kerja + */ + public function getKategoriBadgeClassAttribute() + { + return $this->kategori_kerja === 'Kerja Biasa' ? 'badge-success' : 'badge-warning'; + } + + /** + * Logika terlambat ditiadakan sesuai permintaan user + */ + public function getIsTerlambatAttribute() + { + return false; + } + + /** + * Accessor untuk CSS class badge berdasarkan status + */ + public function getStatusBadgeClassAttribute() + { + $classes = [ + self::STATUS_HADIR => 'badge-success', + self::STATUS_SAKIT => 'badge-warning', + self::STATUS_IZIN => 'badge-info' + ]; + + return $classes[$this->status] ?? 'badge-secondary'; + } + + /** + * Method untuk mengecek apakah absensi sudah lengkap (ada jam masuk dan keluar) + */ + public function isComplete() + { + return !empty($this->jam_masuk) && !empty($this->jam_keluar); + } + + /** + * Method untuk mengecek apakah sudah absen masuk + */ + public function hasAbsenMasuk() + { + return !empty($this->jam_masuk); + } + + /** + * Method untuk mengecek apakah sudah absen keluar + */ + public function hasAbsenKeluar() + { + return !empty($this->jam_keluar); + } + + /** + * Static method untuk mendapatkan statistik absensi berdasarkan periode + */ + public static function getStatistik($startDate = null, $endDate = null) + { + $query = self::query(); + + if ($startDate && $endDate) { + $query->whereBetween('tanggal', [$startDate, $endDate]); + } + + return [ + 'total' => $query->count(), + 'hadir' => $query->where('status', self::STATUS_HADIR)->count(), + 'sakit' => $query->where('status', self::STATUS_SAKIT)->count(), + 'izin' => $query->where('status', self::STATUS_IZIN)->count(), + ]; + } + + /** + * Boot method untuk event model + */ + protected static function boot() + { + parent::boot(); + + // Event ketika model akan dihapus + static::deleting(function ($absensi) { + // Hapus file foto jika ada + if ($absensi->foto_absen_masuk && Storage::disk('public')->exists($absensi->foto_absen_masuk)) { + Storage::disk('public')->delete($absensi->foto_absen_masuk); + } + + if ($absensi->foto_absen_keluar && Storage::disk('public')->exists($absensi->foto_absen_keluar)) { + Storage::disk('public')->delete($absensi->foto_absen_keluar); + } + }); + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Models/AkunTeknisi.php b/samooapk/laravel/app/Models/AkunTeknisi.php new file mode 100644 index 0000000..3286746 --- /dev/null +++ b/samooapk/laravel/app/Models/AkunTeknisi.php @@ -0,0 +1,89 @@ + + */ + protected $fillable = [ + 'id_teknisi', + 'username', + 'password', + 'password_plain', + 'status', + ]; + + /** + * Atribut yang harus disembunyikan (hidden) dari array atau JSON. + * + * @var array + */ + protected $hidden = [ + 'password', + ]; + + /** + * Atribut yang harus dikonversi ke tipe data tertentu. + * + * @var array + */ + protected $casts = [ + 'password' => 'hashed', + ]; + + /** + * Relasi ke model Teknisi. + * Sebuah AkunTeknisi dimiliki oleh satu Teknisi. + * + * @return BelongsTo + */ + public function teknisi(): BelongsTo + { + return $this->belongsTo(Teknisi::class, 'id_teknisi', 'id_teknisi'); + } + + /** + * Get the identifier that will be stored in the subject claim of the JWT. + * + * @return mixed + */ + public function getJWTIdentifier() + { + return $this->getKey(); + } + + /** + * Return a key value array, containing any custom claims to be added to the JWT. + * + * @return array + */ + public function getJWTCustomClaims() + { + return []; + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Models/DashboardStat.php b/samooapk/laravel/app/Models/DashboardStat.php new file mode 100644 index 0000000..9e7589f --- /dev/null +++ b/samooapk/laravel/app/Models/DashboardStat.php @@ -0,0 +1,21 @@ + 0, + 'teknisiAktif' => 0, + 'totalPekerjaan' => 0, + 'penugasanAktif' => 0, + 'chartPenugasan' => [0,0,0,0,0,0,0,0,0,0,0,0], + 'chartSelesai' => [0,0,0,0,0,0,0,0,0,0,0,0], + 'kontrakJatuhTempo' => collect(), + ]); + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Models/DetailPenggajian.php b/samooapk/laravel/app/Models/DetailPenggajian.php new file mode 100644 index 0000000..055ffc1 --- /dev/null +++ b/samooapk/laravel/app/Models/DetailPenggajian.php @@ -0,0 +1,78 @@ + 'decimal:2', + 'jumlah_tim' => 'integer', + 'bagian_ongkos' => 'decimal:2', + 'tanggal_selesai' => 'date', + ]; + + /** + * Relationship with Penggajian + */ + public function penggajian() + { + return $this->belongsTo(Penggajian::class, 'id_penggajian', 'id_penggajian'); + } + + /** + * Relationship with Penugasan + */ + public function penugasan() + { + return $this->belongsTo(Penugasan::class, 'id_penugasan', 'id_penugasan'); + } + + /** + * Boot method to update parent totals + */ + protected static function boot() + { + parent::boot(); + + static::saved(function ($detail) { + $detail->updatePenggajianTotals(); + }); + + static::deleted(function ($detail) { + $detail->updatePenggajianTotals(); + }); + } + + public function updatePenggajianTotals() + { + $penggajian = $this->penggajian; + if ($penggajian) { + $totalOngkos = self::where('id_penggajian', $this->id_penggajian)->sum('bagian_ongkos'); + $biayaMakan = $penggajian->biaya_makan ?? 0; + $potongan = $penggajian->total_potongan ?? $penggajian->total_kasbon ?? 0; + $penggajian->update([ + 'total_ongkos_pekerjaan' => $totalOngkos, + 'gaji_bersih' => ($totalOngkos + $biayaMakan) - $potongan, + ]); + } + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Models/Kasbon.php b/samooapk/laravel/app/Models/Kasbon.php new file mode 100644 index 0000000..c33900b --- /dev/null +++ b/samooapk/laravel/app/Models/Kasbon.php @@ -0,0 +1,192 @@ +tanggal_kasbon) { + $date = \Carbon\Carbon::parse($kasbon->tanggal_kasbon); + $kasbon->periode_bulan = $date->month; + $kasbon->periode_tahun = $date->year; + } + if (auth()->check()) { + $kasbon->diinput_oleh = auth()->id(); + } + }); + } + + /** + * The table associated with the model. + * + * @var string + */ + protected $table = 'kasbons'; + + /** + * The primary key associated with the table. + * + * @var string + */ + protected $primaryKey = 'id_kasbon'; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'id_teknisi', + 'jumlah_kasbon', + 'tanggal_kasbon', + 'periode_bulan', + 'periode_tahun', + 'keperluan', + 'keterangan_detail', + 'metode_pemberian', + 'diinput_oleh', + 'status', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'jumlah_kasbon' => 'decimal:2', + 'tanggal_kasbon' => 'date', + 'status' => 'string', + 'created_at' => 'timestamp', + 'updated_at' => 'timestamp', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var array + */ + protected $hidden = []; + + /** + * Status constants + */ + const STATUS_LUNAS = 'lunas'; + const STATUS_BELUM_LUNAS = 'belum_lunas'; + + /** + * Get all available status options + * + * @return array + */ + public static function getStatusOptions() + { + return [ + self::STATUS_LUNAS => 'Lunas', + self::STATUS_BELUM_LUNAS => 'Belum Lunas' + ]; + } + + /** + * Scope untuk filter berdasarkan status + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string $status + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeByStatus($query, $status) + { + return $query->where('status', $status); + } + + /** + * Scope untuk kasbon yang belum lunas + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeBelumLunas($query) + { + return $query->where('status', self::STATUS_BELUM_LUNAS); + } + + /** + * Scope untuk kasbon yang sudah lunas + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeLunas($query) + { + return $query->where('status', self::STATUS_LUNAS); + } + + /** + * Accessor untuk format jumlah kasbon dalam rupiah + * + * @return string + */ + public function getJumlahKasbonFormatAttribute() + { + return 'Rp ' . number_format($this->jumlah_kasbon, 0, ',', '.'); + } + + /** + * Accessor untuk format tanggal kasbon + * + * @return string + */ + public function getTanggalKasbonFormatAttribute() + { + return $this->tanggal_kasbon ? $this->tanggal_kasbon->format('d/m/Y') : ''; + } + + /** + * Accessor untuk status dalam bahasa Indonesia + * + * @return string + */ + public function getStatusLabelAttribute() + { + $statusOptions = self::getStatusOptions(); + return $statusOptions[$this->status] ?? $this->status; + } + + /** + * Mutator untuk format jumlah kasbon sebelum disimpan + * + * @param mixed $value + * @return void + */ + public function setJumlahKasbonAttribute($value) + { + // Hapus format rupiah jika ada + $cleanValue = str_replace(['Rp', '.', ',', ' '], '', $value); + $this->attributes['jumlah_kasbon'] = (float) $cleanValue; + } + + /** + * Accessor untuk keterangan (alias keperluan) + */ + public function getKeteranganAttribute() + { + return $this->keperluan; + } + + /** + * Relasi ke Teknisi + */ + public function teknisi() + { + return $this->belongsTo(Teknisi::class, 'id_teknisi', 'id_teknisi'); + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Models/Laporan.php b/samooapk/laravel/app/Models/Laporan.php new file mode 100644 index 0000000..7d36e2f --- /dev/null +++ b/samooapk/laravel/app/Models/Laporan.php @@ -0,0 +1,228 @@ + DB::table('kasbons')->count(), + 'total_teknisi' => DB::table('teknisis')->count(), + 'total_pekerjaan' => DB::table('penugasans')->count(), + 'total_absensi' => DB::table('absensis') + ->whereMonth('tanggal', date('m')) + ->whereYear('tanggal', date('Y')) + ->count(), + + // Kasbon statistics + 'total_lunas' => DB::table('kasbons') + ->where('status', 'lunas') + ->sum('jumlah_kasbon'), + 'total_belum_lunas' => DB::table('kasbons') + ->where('status', 'belum_lunas') + ->sum('jumlah_kasbon'), + + // Teknisi statistics + 'teknisi_aktif' => DB::table('teknisis') + ->where('status', 'aktif') + ->count(), + 'teknisi_nonaktif' => DB::table('teknisis') + ->where('status', 'nonaktif') + ->count(), + + // Absensi statistics + 'hadir' => DB::table('absensis') + ->where('status', 'hadir') + ->whereMonth('tanggal', date('m')) + ->whereYear('tanggal', date('Y')) + ->count(), + 'izin' => DB::table('absensis') + ->where('status', 'izin') + ->whereMonth('tanggal', date('m')) + ->whereYear('tanggal', date('Y')) + ->count(), + 'sakit' => DB::table('absensis') + ->where('status', 'sakit') + ->whereMonth('tanggal', date('m')) + ->whereYear('tanggal', date('Y')) + ->count(), + 'alpha' => DB::table('absensis') + ->where('status', 'alpha') + ->whereMonth('tanggal', date('m')) + ->whereYear('tanggal', date('Y')) + ->count(), + + // Pekerjaan statistics - using penugasans table + 'selesai' => DB::table('penugasans') + ->where('status_pekerjaan', 'selesai') + ->count(), + 'progress' => DB::table('penugasans') + ->where('status_pekerjaan', 'proses') + ->count(), + 'pending' => DB::table('penugasans') + ->where('status_pekerjaan', 'pending') + ->count(), + ]; + } + + /** + * Get kasbon data with filters + */ + public static function getKasbonData($filters = []) + { + $query = DB::table('kasbons') + ->leftJoin('teknisis', 'kasbons.id_teknisi', '=', 'teknisis.id_teknisi') + ->select( + 'kasbons.*', + 'teknisis.nama as nama_teknisi' + ); + + if (!empty($filters['search'])) { + $query->where(function($q) use ($filters) { + $q->where('teknisis.nama', 'like', '%' . $filters['search'] . '%') + ->orWhere('kasbons.keperluan', 'like', '%' . $filters['search'] . '%'); + }); + } + + if (!empty($filters['tanggal_dari'])) { + $query->whereDate('kasbons.tanggal_kasbon', '>=', $filters['tanggal_dari']); + } + + if (!empty($filters['tanggal_sampai'])) { + $query->whereDate('kasbons.tanggal_kasbon', '<=', $filters['tanggal_sampai']); + } + + if (!empty($filters['status'])) { + $query->where('kasbons.status', $filters['status']); + } + + if (!empty($filters['id_teknisi'])) { + $query->where('kasbons.id_teknisi', $filters['id_teknisi']); + } + + return $query->orderBy('kasbons.tanggal_kasbon', 'desc'); + } + + /** + * Get teknisi data with filters + */ + public static function getTeknisiData($filters = []) + { + // Use raw SQL for groupBy to avoid MySQL strict mode issues + $query = DB::table('teknisis') + ->leftJoin('absensis', function($join) use ($filters) { + $join->on('teknisis.id_teknisi', '=', 'absensis.id_teknisi'); + if (!empty($filters['tanggal_dari'])) { + $join->whereDate('absensis.tanggal', '>=', $filters['tanggal_dari']); + } + if (!empty($filters['tanggal_sampai'])) { + $join->whereDate('absensis.tanggal', '<=', $filters['tanggal_sampai']); + } + }) + ->select( + 'teknisis.id_teknisi', + 'teknisis.nama', + 'teknisis.status', + DB::raw('COUNT(absensis.id_absensi) as total_absensi'), + DB::raw('COUNT(CASE WHEN absensis.status = "hadir" THEN 1 END) as hadir'), + DB::raw('COUNT(CASE WHEN absensis.status = "izin" THEN 1 END) as izin'), + DB::raw('COUNT(CASE WHEN absensis.status = "sakit" THEN 1 END) as sakit'), + DB::raw('COUNT(CASE WHEN absensis.status = "alpha" THEN 1 END) as alpha') + ) + ->groupBy('teknisis.id_teknisi', 'teknisis.nama', 'teknisis.status'); + + if (!empty($filters['search'])) { + $query->where('teknisis.nama', 'like', '%' . $filters['search'] . '%'); + } + + return $query->orderBy('teknisis.nama', 'asc'); + } + + /** + * Get absensi data with filters + */ + public static function getAbsensiData($filters = []) + { + $query = DB::table('absensis') + ->leftJoin('teknisis', 'absensis.id_teknisi', '=', 'teknisis.id_teknisi') + ->select( + 'absensis.*', + 'teknisis.nama as nama_teknisi' + ); + + if (!empty($filters['search'])) { + $query->where('teknisis.nama', 'like', '%' . $filters['search'] . '%'); + } + + if (!empty($filters['tanggal_dari'])) { + $query->whereDate('absensis.tanggal', '>=', $filters['tanggal_dari']); + } + + if (!empty($filters['tanggal_sampai'])) { + $query->whereDate('absensis.tanggal', '<=', $filters['tanggal_sampai']); + } + + if (!empty($filters['status'])) { + $query->where('absensis.status', $filters['status']); + } + + if (!empty($filters['id_teknisi'])) { + $query->where('absensis.id_teknisi', $filters['id_teknisi']); + } + + return $query->orderBy('absensis.tanggal', 'desc'); + } + + /** + * Get pekerjaan data with filters + */ + public static function getPekerjaanData($filters = []) + { + $query = DB::table('penugasans') + ->leftJoin('teknisis', 'penugasans.id_teknisi', '=', 'teknisis.id_teknisi') + ->select( + 'penugasans.*', + 'teknisis.nama as nama_teknisi' + ); + + if (!empty($filters['search'])) { + $query->where(function($q) use ($filters) { + $q->where('teknisis.nama', 'like', '%' . $filters['search'] . '%') + ->orWhere('penugasans.jenis_pekerjaan', 'like', '%' . $filters['search'] . '%') + ->orWhere('penugasans.catatan_admin', 'like', '%' . $filters['search'] . '%'); + }); + } + + if (!empty($filters['tanggal_dari'])) { + $query->whereDate('penugasans.tanggal_mulai', '>=', $filters['tanggal_dari']); + } + + if (!empty($filters['tanggal_sampai'])) { + $query->whereDate('penugasans.tanggal_diselesaikan', '<=', $filters['tanggal_sampai']); + } + + if (!empty($filters['status'])) { + $query->where('penugasans.status_pekerjaan', $filters['status']); + } + + if (!empty($filters['id_teknisi'])) { + $query->where('penugasans.id_teknisi', $filters['id_teknisi']); + } + + return $query->orderBy('penugasans.created_at', 'desc'); + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Models/Penggajian.php b/samooapk/laravel/app/Models/Penggajian.php new file mode 100644 index 0000000..9d8ddf3 --- /dev/null +++ b/samooapk/laravel/app/Models/Penggajian.php @@ -0,0 +1,383 @@ + 'Januari', 2 => 'Februari', 3 => 'Maret', 4 => 'April', + 5 => 'Mei', 6 => 'Juni', 7 => 'Juli', 8 => 'Agustus', + 9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Desember' + ]; + return $bulan[(int)$angka] ?? 'N/A'; + } + + /** + * The table associated with the model. + */ + protected $table = 'penggajians'; + + /** + * The primary key for the model. + */ + protected $primaryKey = 'id_penggajian'; + + /** + * The attributes that are mass assignable. + */ + protected $fillable = [ + 'id_teknisi', + 'periode_bulan', + 'periode_tahun', + 'tanggal_penggajian', + 'total_ongkos_pekerjaan', + 'jumlah_penugasan_selesai', + 'jumlah_hari_kerja', + 'total_kasbon', + 'biaya_makan', + 'total_potongan', + 'gaji_bersih', + 'status_pembayaran', + 'metode_pembayaran', + 'tanggal_dibayar', + 'bukti_pembayaran', + 'catatan', + ]; + + /** + * The attributes that should be cast. + */ + protected $casts = [ + 'tanggal_penggajian' => 'date', + 'jumlah_hari_kerja' => 'integer', + 'total_ongkos_pekerjaan' => 'decimal:2', + 'total_kasbon' => 'decimal:2', + 'biaya_makan' => 'decimal:2', + 'total_potongan' => 'decimal:2', + 'gaji_bersih' => 'decimal:2', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + /** + * The attributes that should be hidden for arrays. + */ + protected $hidden = [ + 'deleted_at', + ]; + + /** + * Status pembayaran constants + */ + const STATUS_BELUM_BAYAR = 'belum_bayar'; + const STATUS_SUDAH_BAYAR = 'sudah_bayar'; + + /** + * Get all available status pembayaran + */ + public static function getStatusPembayaran() + { + return [ + self::STATUS_BELUM_BAYAR => 'Belum Bayar', + self::STATUS_SUDAH_BAYAR => 'Sudah Bayar', + ]; + } + + /** + * Relationship with Teknisi + */ + public function teknisi() + { + return $this->belongsTo(Teknisi::class, 'id_teknisi', 'id_teknisi'); + } + + /** + * Relationship with DetailPenggajian + */ + public function detailPenggajian() + { + return $this->hasMany(DetailPenggajian::class, 'id_penggajian', 'id_penggajian'); + } + + /** + * Scope for filtering by periode + */ + public function scopeByPeriode($query, $bulan = null, $tahun = null) + { + if ($bulan) { + $query->where('periode_bulan', $bulan); + } + if ($tahun) { + $query->where('periode_tahun', $tahun); + } + return $query; + } + + /** + * Scope for filtering by status pembayaran + */ + public function scopeByStatus($query, $status) + { + return $query->where('status_pembayaran', $status); + } + + /** + * Scope for belum bayar + */ + public function scopeBelumBayar($query) + { + return $query->where('status_pembayaran', self::STATUS_BELUM_BAYAR); + } + + /** + * Scope for sudah bayar + */ + public function scopeSudahBayar($query) + { + return $query->where('status_pembayaran', self::STATUS_SUDAH_BAYAR); + } + + /** + * Scope for current month + */ + public function scopeCurrentMonth($query) + { + return $query->where('periode_bulan', date('n')) + ->where('periode_tahun', date('Y')); + } + + /** + * Scope for latest periode + */ + public function scopeLatestPeriode($query) + { + return $query->orderBy('periode_tahun', 'desc') + ->orderBy('periode_bulan', 'desc'); + } + + /** + * Get formatted periode + */ + public function getFormattedPeriodeAttribute() + { + $bulan = Carbon::create()->month($this->periode_bulan)->format('F'); + return $bulan . ' ' . $this->periode_tahun; + } + + /** + * Get periode short format + */ + public function getPeriodeShortAttribute() + { + $bulan = Carbon::create()->month($this->periode_bulan)->format('M'); + return $bulan . ' ' . $this->periode_tahun; + } + + /** + * Get status pembayaran label + */ + public function getStatusLabelAttribute() + { + $statuses = self::getStatusPembayaran(); + return $statuses[$this->status_pembayaran] ?? $this->status_pembayaran; + } + + /** + * Get status badge class + */ + public function getStatusBadgeClassAttribute() + { + switch ($this->status_pembayaran) { + case self::STATUS_SUDAH_BAYAR: + return 'bg-success'; + case self::STATUS_BELUM_BAYAR: + default: + return 'bg-warning'; + } + } + + /** + * Check if already paid + */ + public function isPaid() + { + return $this->status_pembayaran === self::STATUS_SUDAH_BAYAR; + } + + /** + * Check if not paid yet + */ + public function isUnpaid() + { + return $this->status_pembayaran === self::STATUS_BELUM_BAYAR; + } + + /** + * Mark as paid + */ + public function markAsPaid() + { + $this->update([ + 'status_pembayaran' => self::STATUS_SUDAH_BAYAR + ]); + } + + /** + * Mark as unpaid + */ + public function markAsUnpaid() + { + $this->update([ + 'status_pembayaran' => self::STATUS_BELUM_BAYAR + ]); + } + + /** + * Calculate total from detail penggajian + */ + public function calculateTotals() + { + $details = $this->detailPenggajian; + + $totals = [ + 'subtotal' => $details->sum('subtotal'), + 'total_unit' => $details->sum('jumlah_unit'), + ]; + + return $totals; + } + + /** + * Get formatted currency attributes + */ + public function getFormattedGajiKotorAttribute() + { + return 'Rp ' . number_format($this->total_ongkos_pekerjaan, 0, ',', '.'); + } + + public function getFormattedBiayaMakanAttribute() + { + return 'Rp ' . number_format($this->biaya_makan, 0, ',', '.'); + } + + public function getFormattedTotalKasbonAttribute() + { + return 'Rp ' . number_format($this->total_kasbon, 0, ',', '.'); + } + + public function getFormattedGajiBersihAttribute() + { + return 'Rp ' . number_format($this->gaji_bersih, 0, ',', '.'); + } + + /** + * Get potongan (total kasbon) + */ + public function getPotonganAttribute() + { + return $this->total_kasbon; + } + + /** + * Get formatted potongan + */ + public function getFormattedPotonganAttribute() + { + return 'Rp ' . number_format($this->potongan, 0, ',', '.'); + } + + /** + * Static method to get summary by periode + */ + public static function getSummaryByPeriode($bulan = null, $tahun = null, $status = null) + { + $query = self::with('teknisi'); + + if ($bulan) { + $query->where('periode_bulan', $bulan); + } + if ($tahun) { + $query->where('periode_tahun', $tahun); + } + if ($status) { + $query->where('status_pembayaran', $status); + } + + $data = $query->get(); + + return [ + 'total_teknisi' => $data->count(), + 'total_gaji' => $data->sum('total_ongkos_pekerjaan'), + 'total_kasbon' => $data->sum('total_kasbon'), + 'total_biaya_makan' => $data->sum('biaya_makan'), + 'gaji_bersih' => $data->sum('gaji_bersih'), + 'belum_bayar' => $data->where('status_pembayaran', self::STATUS_BELUM_BAYAR)->count(), + 'sudah_bayar' => $data->where('status_pembayaran', self::STATUS_SUDAH_BAYAR)->count(), + ]; + } + + /** + * Static method to check if periode already exists for teknisi + */ + public static function isPeriodeExists($idTeknisi, $bulan, $tahun) + { + return self::where('id_teknisi', $idTeknisi) + ->where('periode_bulan', $bulan) + ->where('periode_tahun', $tahun) + ->exists(); + } + + /** + * Static method to get latest periode + */ + public static function getLatestPeriode() + { + $latest = self::orderBy('periode_tahun', 'desc') + ->orderBy('periode_bulan', 'desc') + ->first(); + + if (!$latest) { + return [ + 'bulan' => date('n'), + 'tahun' => date('Y') + ]; + } + + return [ + 'bulan' => $latest->periode_bulan, + 'tahun' => $latest->periode_tahun + ]; + } + + /** + * Boot method + */ + protected static function boot() + { + parent::boot(); + + // Auto calculate gaji_bersih before saving + static::saving(function ($penggajian) { + $ongkos = $penggajian->total_ongkos_pekerjaan ?? 0; + $makan = $penggajian->biaya_makan ?? 0; + $potongan = $penggajian->total_potongan ?? 0; + + // Gaji Bersih = Ongkos - Uang Makan - Potongan Kasbon + // (Uang makan sekarang dihitung sebagai potongan) + $penggajian->gaji_bersih = $ongkos - $makan - $potongan; + }); + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Models/Penugasan.php b/samooapk/laravel/app/Models/Penugasan.php new file mode 100644 index 0000000..5801115 --- /dev/null +++ b/samooapk/laravel/app/Models/Penugasan.php @@ -0,0 +1,202 @@ + 'date', + 'tanggal_mulai' => 'datetime', + 'tanggal_diselesaikan' => 'datetime', + 'tanggal_garansi_mulai' => 'date', + 'tanggal_garansi_selesai' => 'date', + 'jarak_meter' => 'decimal:2', + 'total_nilai_pekerjaan' => 'decimal:2', + 'pakai_pipa_besi' => 'boolean', + ]; + + // =================================== + // RELATIONSHIPS + // =================================== + + public function teknisi() + { + return $this->belongsTo(Teknisi::class, 'id_teknisi', 'id_teknisi'); + } + + public function teknisiPertama() + { + return $this->teknisi(); + } + + public function tarif() + { + return $this->belongsTo(TarifPekerjaan::class, 'id_tarif', 'id_tarif'); + } + + public function timTeknisi() + { + return $this->hasMany(TimTeknisiPenugasan::class, 'id_penugasan', 'id_penugasan'); + } + + public function items() + { + return $this->hasMany(PenugasanItem::class, 'id_penugasan', 'id_penugasan'); + } + + // =================================== + // ACCESSOR - FOTO URL + // =================================== + + public function getFotoSuratUrlAttribute(): ?string + { + if (!$this->foto_surat) return null; + return url('storage/' . $this->foto_surat); // ✅ url() bukan asset() + } + + public function getFotoSebelumUrlAttribute(): ?string + { + if (!$this->foto_sebelum) return null; + return url('storage/' . $this->foto_sebelum); + } + + public function getFotoSesudahUrlAttribute(): ?string + { + if (!$this->foto_sesudah) return null; + return url('storage/' . $this->foto_sesudah); + } + + // =================================== + // HELPER - TIM + // =================================== + + public function getAllTimTeknisi() + { + return $this->timTeknisi()->with('teknisi')->get(); + } + + public function countTotalTim(): int + { + return $this->timTeknisi()->count(); + } + + public function countTimHadir(): int + { + return $this->timTeknisi()->where('status_kehadiran', 'hadir')->count(); + } + + public function getOngkosPerOrang(): float + { + $jumlahHadir = $this->countTimHadir(); + if ($jumlahHadir === 0 || !$this->total_nilai_pekerjaan) return 0; + return $this->total_nilai_pekerjaan / $jumlahHadir; + } + + // =================================== + // HELPER - GARANSI + // =================================== + + public function setGaransiMeteranAir($tanggalMulai = null) + { + $mulai = $tanggalMulai ? Carbon::parse($tanggalMulai) : Carbon::now(); + $this->tanggal_garansi_mulai = $mulai; + $this->tanggal_garansi_selesai = $mulai->copy()->addMonths(3); + $this->catatan_garansi = 'Garansi 3 bulan untuk pemasangan meteran air (SR)'; + } + + public function isGaransiAktif(): bool + { + if (!$this->tanggal_garansi_mulai || !$this->tanggal_garansi_selesai) return false; + return now()->between($this->tanggal_garansi_mulai, $this->tanggal_garansi_selesai); + } + + public function getSisaHariGaransi(): ?int + { + if (!$this->tanggal_garansi_selesai) return null; + $sisa = now()->diffInDays($this->tanggal_garansi_selesai, false); + return $sisa > 0 ? (int)$sisa : 0; + } + + public function scopeGaransiAktif($query) + { + return $query->whereNotNull('tanggal_garansi_mulai') + ->whereNotNull('tanggal_garansi_selesai') + ->where('tanggal_garansi_selesai', '>=', now()); + } + + // =================================== + // HELPER - STATUS + // =================================== + + public function isDetailLengkap(): bool + { + return !is_null($this->jenis_pekerjaan); + } + + public function isSelesai(): bool + { + return $this->status_pekerjaan === 'selesai'; + } + + public function getLabelJenisPekerjaanAttribute(): string + { + $labels = [ + 'sr' => 'SR (Sambungan Rumah)', + 'pengembangan_jaringan_pipa' => 'Pengembangan Jaringan Pipa', + 'pengangkatan' => 'Pengangkatan', + 'pemasangan_gate_valve' => 'Pemasangan Gate Valve', + 'gali_urug' => 'Gali Urug', + 'perbaikan_jaringan_pipa' => 'Perbaikan Jaringan Pipa', + 'pengecatan_pipa_besi' => 'Pengecatan Pipa Besi', + 'penyempurnaan_jaringan_pipa' => 'Penyempurnaan Jaringan Pipa', + ]; + return $labels[$this->jenis_pekerjaan] ?? '-'; + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Models/PenugasanItem.php b/samooapk/laravel/app/Models/PenugasanItem.php new file mode 100644 index 0000000..267882d --- /dev/null +++ b/samooapk/laravel/app/Models/PenugasanItem.php @@ -0,0 +1,50 @@ +belongsTo(Penugasan::class, 'id_penugasan', 'id_penugasan'); + } + + public function tarif() + { + return $this->belongsTo(TarifPekerjaan::class, 'id_tarif', 'id_tarif'); + } + + public function getLabelJenisPekerjaanAttribute(): string + { + $labels = [ + 'sr' => 'SR (Sambungan Rumah)', + 'pengembangan_jaringan_pipa' => 'Pengembangan Jaringan Pipa', + 'pengangkatan' => 'Pengangkatan', + 'pemasangan_gate_valve' => 'Pemasangan Gate Valve', + 'gali_urug' => 'Gali Urug', + 'perbaikan_jaringan_pipa' => 'Perbaikan Jaringan Pipa', + 'pengecatan_pipa_besi' => 'Pengecatan Pipa Besi', + 'penyempurnaan_jaringan_pipa' => 'Penyempurnaan Jaringan Pipa', + ]; + return $labels[$this->jenis_pekerjaan] ?? $this->jenis_pekerjaan; + } +} diff --git a/samooapk/laravel/app/Models/ProgresKerja.php b/samooapk/laravel/app/Models/ProgresKerja.php new file mode 100644 index 0000000..8d0da01 --- /dev/null +++ b/samooapk/laravel/app/Models/ProgresKerja.php @@ -0,0 +1,254 @@ + 'datetime', + 'persentase_pekerjaan' => 'integer', + ]; + + // Status progres options + public static $statusProgres = [ + 'belum_mulai' => 'Belum Mulai', + 'dalam_progres' => 'Dalam Progres', + 'terhenti' => 'Terhenti', + 'selesai' => 'Selesai', + 'ditunda' => 'Ditunda' + ]; + + // Boot method untuk auto update tanggal_update + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + $model->tanggal_update = now(); + }); + + static::updating(function ($model) { + $model->tanggal_update = now(); + }); + } + + // Relationships + public function teknisi() + { + return $this->belongsTo(Teknisi::class, 'id_teknisi', 'id_teknisi'); + } + + public function penugasan() + { + return $this->belongsTo(Penugasan::class, 'id_penugasan', 'id_penugasan'); + } + + // Scopes + public function scopeByStatus($query, $status) + { + return $query->where('status_progres', $status); + } + + public function scopeByTeknisi($query, $teknisiId) + { + return $query->where('id_teknisi', $teknisiId); + } + + public function scopeByDateRange($query, $startDate = null, $endDate = null) + { + if ($startDate) { + $query->whereDate('tanggal_update', '>=', $startDate); + } + + if ($endDate) { + $query->whereDate('tanggal_update', '<=', $endDate); + } + + return $query; + } + + // Accessors + public function getStatusFormattedAttribute() + { + return self::$statusProgres[$this->status_progres] ?? 'Unknown'; + } + + public function getStatusBadgeClassAttribute() + { + $badgeClasses = [ + 'belum_mulai' => 'badge-secondary', + 'dalam_progres' => 'badge-primary', + 'terhenti' => 'badge-danger', + 'selesai' => 'badge-success', + 'ditunda' => 'badge-warning' + ]; + + return $badgeClasses[$this->status_progres] ?? 'badge-secondary'; + } + + public function getTanggalUpdateFormattedAttribute() + { + return $this->tanggal_update ? $this->tanggal_update->format('d/m/Y H:i') : '-'; + } + + public function getHasFotoAttribute() + { + return !empty($this->foto_progress); + } + + public function getFotoProgressUrlAttribute() + { + if ($this->foto_progress) { + return asset('storage/progress_photos/' . $this->foto_progress); + } + return null; + } + + public function getDaysSinceUpdateAttribute() + { + if ($this->tanggal_update) { + return $this->tanggal_update->diffInDays(now()); + } + return 0; + } + + // Methods + public function autoUpdateStatus() + { + $percentage = $this->persentase_pekerjaan; + + if ($percentage == 0) { + $newStatus = 'belum_mulai'; + } elseif ($percentage > 0 && $percentage < 100) { + $newStatus = 'dalam_progres'; + } elseif ($percentage == 100) { + $newStatus = 'selesai'; + } else { + return; // No change needed + } + + // Only update if status is different + if ($this->status_progres !== $newStatus) { + $this->update(['status_progres' => $newStatus]); + } + } + + public function canTransitionTo($newStatus) + { + $allowedTransitions = [ + 'belum_mulai' => ['dalam_progres', 'ditunda'], + 'dalam_progres' => ['terhenti', 'selesai', 'ditunda'], + 'terhenti' => ['dalam_progres', 'ditunda'], + 'selesai' => [], // Cannot transition from completed + 'ditunda' => ['belum_mulai', 'dalam_progres'] + ]; + + $currentStatus = $this->status_progres; + + return in_array($newStatus, $allowedTransitions[$currentStatus] ?? []); + } + + // Static methods + public static function getByTeknisi($teknisiId) + { + return self::with(['teknisi', 'penugasan']) + ->where('id_teknisi', $teknisiId) + ->orderBy('tanggal_update', 'desc') + ->get(); + } + + public static function getRecentUpdates($limit = 10) + { + return self::with(['teknisi', 'penugasan']) + ->orderBy('tanggal_update', 'desc') + ->limit($limit) + ->get(); + } + + public static function getStatistics() + { + $total = self::count(); + $byStatus = self::selectRaw('status_progres, COUNT(*) as count') + ->groupBy('status_progres') + ->pluck('count', 'status_progres') + ->toArray(); + + $statistics = [ + 'total' => $total, + 'belum_mulai' => $byStatus['belum_mulai'] ?? 0, + 'dalam_progres' => $byStatus['dalam_progres'] ?? 0, + 'terhenti' => $byStatus['terhenti'] ?? 0, + 'selesai' => $byStatus['selesai'] ?? 0, + 'ditunda' => $byStatus['ditunda'] ?? 0, + ]; + + // Add percentages + if ($total > 0) { + $statistics['completion_rate'] = round(($statistics['selesai'] / $total) * 100, 1); + $statistics['in_progress_rate'] = round(($statistics['dalam_progres'] / $total) * 100, 1); + } else { + $statistics['completion_rate'] = 0; + $statistics['in_progress_rate'] = 0; + } + + return $statistics; + } + + public static function getOverdueProgress($days = 7) + { + return self::with(['teknisi', 'penugasan']) + ->whereIn('status_progres', ['dalam_progres', 'terhenti']) + ->where('tanggal_update', '<', now()->subDays($days)) + ->orderBy('tanggal_update', 'asc') + ->get(); + } + + public static function getProgressSummaryByTeknisi() + { + return self::with('teknisi') + ->selectRaw('id_teknisi, status_progres, COUNT(*) as count') + ->groupBy('id_teknisi', 'status_progres') + ->get() + ->groupBy('id_teknisi') + ->map(function ($items) { + $teknisi = $items->first()->teknisi; + $summary = [ + 'teknisi_id' => $teknisi->id_teknisi, + 'teknisi_nama' => $teknisi->nama, + 'total' => $items->sum('count'), + 'belum_mulai' => 0, + 'dalam_progres' => 0, + 'terhenti' => 0, + 'selesai' => 0, + 'ditunda' => 0, + ]; + + foreach ($items as $item) { + $summary[$item->status_progres] = $item->count; + } + + return $summary; + }) + ->values(); + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Models/TarifPekerjaan.php b/samooapk/laravel/app/Models/TarifPekerjaan.php new file mode 100644 index 0000000..01a4e72 --- /dev/null +++ b/samooapk/laravel/app/Models/TarifPekerjaan.php @@ -0,0 +1,61 @@ + 'decimal:2', + 'tarif_per_meter' => 'decimal:2', + 'pakai_pipa_besi' => 'boolean', + 'ada_garansi' => 'boolean', + 'is_active' => 'boolean', + ]; + + // Accessor: ambil harga (tarif_per_unit atau tarif_per_meter) + public function getHargaAttribute(): float + { + return (float) ($this->tarif_per_unit ?? $this->tarif_per_meter ?? 0); + } + + // Relationship + public function penugasans() + { + return $this->hasMany(Penugasan::class, 'id_tarif', 'id_tarif'); + } + + // Scope untuk filter aktif + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + // Scope untuk filter by kategori + public function scopeKategori($query, $kategori) + { + return $query->where('kategori', $kategori); + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Models/Teknisi.php b/samooapk/laravel/app/Models/Teknisi.php new file mode 100644 index 0000000..0e7393b --- /dev/null +++ b/samooapk/laravel/app/Models/Teknisi.php @@ -0,0 +1,88 @@ + + */ + protected $fillable = [ + 'nama', + 'tanggal_lahir', + 'alamat', + 'email', + 'no_telephone', + 'tanggal_masuk', + 'status', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'tanggal_lahir' => 'date', + 'tanggal_masuk' => 'date', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var array + */ + protected $hidden = []; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'tanggal_lahir' => 'date', + 'tanggal_masuk' => 'date', + ]; + } + + /** + * Scope a query to only include active teknisi. + */ + public function scopeAktif($query) + { + return $query->where('status', 'aktif'); + } + + /** + * Scope a query to only include inactive teknisi. + */ + public function scopeTidakAktif($query) + { + return $query->where('status', 'tidak_aktif'); + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Models/TimTeknisiPenugasan.php b/samooapk/laravel/app/Models/TimTeknisiPenugasan.php new file mode 100644 index 0000000..9fc17aa --- /dev/null +++ b/samooapk/laravel/app/Models/TimTeknisiPenugasan.php @@ -0,0 +1,72 @@ + 'datetime', + 'updated_at' => 'datetime', + ]; + + // =================================== + // RELATIONSHIPS + // =================================== + + /** + * Relasi ke penugasan + */ + public function penugasan() + { + return $this->belongsTo(Penugasan::class, 'id_penugasan', 'id_penugasan'); + } + + /** + * Relasi ke teknisi + */ + public function teknisi() + { + return $this->belongsTo(Teknisi::class, 'id_teknisi', 'id_teknisi'); + } + + // =================================== + // HELPER METHODS + // =================================== + + /** + * Cek apakah teknisi hadir + */ + public function isHadir(): bool + { + return $this->status_kehadiran === 'hadir'; + } + + /** + * Cek apakah teknisi tidak hadir + */ + public function isTidakHadir(): bool + { + return $this->status_kehadiran === 'tidak_hadir'; + } + + /** + * Cek apakah teknisi izin + */ + public function isIzin(): bool + { + return $this->status_kehadiran === 'izin'; + } +} \ No newline at end of file diff --git a/samooapk/laravel/app/Models/User.php b/samooapk/laravel/app/Models/User.php new file mode 100644 index 0000000..4d7f70f --- /dev/null +++ b/samooapk/laravel/app/Models/User.php @@ -0,0 +1,45 @@ + + */ + protected $fillable = [ + 'name', + 'email', + 'password', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var array + */ + protected $hidden = [ + 'password', + 'remember_token', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + ]; +} diff --git a/samooapk/laravel/app/Providers/AppServiceProvider.php b/samooapk/laravel/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..55d88cd --- /dev/null +++ b/samooapk/laravel/app/Providers/AppServiceProvider.php @@ -0,0 +1,26 @@ + + */ + protected $policies = [ + // + ]; + + /** + * Register any authentication / authorization services. + */ + public function boot(): void + { + // + } +} diff --git a/samooapk/laravel/app/Providers/BroadcastServiceProvider.php b/samooapk/laravel/app/Providers/BroadcastServiceProvider.php new file mode 100644 index 0000000..2be04f5 --- /dev/null +++ b/samooapk/laravel/app/Providers/BroadcastServiceProvider.php @@ -0,0 +1,19 @@ +> + */ + protected $listen = [ + Registered::class => [ + SendEmailVerificationNotification::class, + ], + ]; + + /** + * Register any events for your application. + */ + public function boot(): void + { + // + } + + /** + * Determine if events and listeners should be automatically discovered. + */ + public function shouldDiscoverEvents(): bool + { + return false; + } +} diff --git a/samooapk/laravel/app/Providers/RouteServiceProvider.php b/samooapk/laravel/app/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..025e874 --- /dev/null +++ b/samooapk/laravel/app/Providers/RouteServiceProvider.php @@ -0,0 +1,40 @@ +by($request->user()?->id ?: $request->ip()); + }); + + $this->routes(function () { + Route::middleware('api') + ->prefix('api') + ->group(base_path('routes/api.php')); + + Route::middleware('web') + ->group(base_path('routes/web.php')); + }); + } +} diff --git a/samooapk/laravel/app/View/Components/AppLayout.php b/samooapk/laravel/app/View/Components/AppLayout.php new file mode 100644 index 0000000..de0d46f --- /dev/null +++ b/samooapk/laravel/app/View/Components/AppLayout.php @@ -0,0 +1,17 @@ +make(Illuminate\Contracts\Console\Kernel::class); + +$status = $kernel->handle( + $input = new Symfony\Component\Console\Input\ArgvInput, + new Symfony\Component\Console\Output\ConsoleOutput +); + +/* +|-------------------------------------------------------------------------- +| Shutdown The Application +|-------------------------------------------------------------------------- +| +| Once Artisan has finished running, we will fire off the shutdown events +| so that any final work may be done by the application before we shut +| down the process. This is the last thing to happen to the request. +| +*/ + +$kernel->terminate($input, $status); + +exit($status); diff --git a/samooapk/laravel/bootstrap/app.php b/samooapk/laravel/bootstrap/app.php new file mode 100644 index 0000000..037e17d --- /dev/null +++ b/samooapk/laravel/bootstrap/app.php @@ -0,0 +1,55 @@ +singleton( + Illuminate\Contracts\Http\Kernel::class, + App\Http\Kernel::class +); + +$app->singleton( + Illuminate\Contracts\Console\Kernel::class, + App\Console\Kernel::class +); + +$app->singleton( + Illuminate\Contracts\Debug\ExceptionHandler::class, + App\Exceptions\Handler::class +); + +/* +|-------------------------------------------------------------------------- +| Return The Application +|-------------------------------------------------------------------------- +| +| This script returns the application instance. The instance is given to +| the calling script so we can separate the building of the instances +| from the actual running of the application and sending responses. +| +*/ + +return $app; diff --git a/samooapk/laravel/bootstrap/cache/.gitignore b/samooapk/laravel/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/samooapk/laravel/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/samooapk/laravel/composer.json b/samooapk/laravel/composer.json new file mode 100644 index 0000000..f161709 --- /dev/null +++ b/samooapk/laravel/composer.json @@ -0,0 +1,68 @@ +{ + "name": "laravel/laravel", + "type": "project", + "description": "The skeleton application for the Laravel framework.", + "keywords": ["laravel", "framework"], + "license": "MIT", + "require": { + "php": "^8.1", + "guzzlehttp/guzzle": "^7.2", + "laravel/framework": "^10.10", + "laravel/sanctum": "^3.3", + "laravel/tinker": "^2.8", + "tymon/jwt-auth": "^2.2" + }, + "require-dev": { + "fakerphp/faker": "^1.9.1", + "laravel/breeze": "^1.29", + "laravel/pint": "^1.0", + "laravel/sail": "^1.18", + "mockery/mockery": "^1.4.4", + "nunomaduro/collision": "^7.0", + "phpunit/phpunit": "^10.1", + "spatie/laravel-ignition": "^2.0" + }, + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Factories\\": "database/factories/", + "Database\\Seeders\\": "database/seeders/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "scripts": { + "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" + ] + }, + "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 +} diff --git a/samooapk/laravel/composer.lock b/samooapk/laravel/composer.lock new file mode 100644 index 0000000..8abc9bf --- /dev/null +++ b/samooapk/laravel/composer.lock @@ -0,0 +1,8507 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "b67700b6aed09023baed8ab1b2e31edc", + "packages": [ + { + "name": "brick/math", + "version": "0.12.3", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba", + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "6.8.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.12.3" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2025-02-28T13:11:00+00:00" + }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", + "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "doctrine/dbal": "<3.7.0 || >=4.0.0" + }, + "require-dev": { + "doctrine/dbal": "^3.7.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/2.1.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2023-12-11T17:09:12+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.0.10", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^11.0", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^8.5 || ^9.5", + "vimeo/psalm": "^4.25 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.0.10" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2024-02-18T20:23:39+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "8c784d071debd117328803d86b2097615b457500" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/8c784d071debd117328803d86b2097615b457500", + "reference": "8c784d071debd117328803d86b2097615b457500", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0", + "webmozart/assert": "^1.0" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.0", + "phpunit/phpunit": "^7.0|^8.0|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2024-10-09T13:47:03+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "symfony/http-foundation": "^4.4|^5.4|^6|^7" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2023-10-12T05:21:21+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:45:45+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.9.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-03-27T13:37:11+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.2.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-03-27T13:27:01+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2025-03-27T12:30:47+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.4", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "30e286560c137526eccd4ce21b2de477ab0676d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2", + "reference": "30e286560c137526eccd4ce21b2de477ab0676d2", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2025-02-03T10:55:03+00:00" + }, + { + "name": "laravel/framework", + "version": "v10.48.29", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "8f7f9247cb8aad1a769d6b9815a6623d89b46b47" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/8f7f9247cb8aad1a769d6b9815a6623d89b46b47", + "reference": "8f7f9247cb8aad1a769d6b9815a6623d89b46b47", + "shasum": "" + }, + "require": { + "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.3.2", + "egulias/email-validator": "^3.2.1|^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.2", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.1.9", + "laravel/serializable-closure": "^1.3", + "league/commonmark": "^2.2.1", + "league/flysystem": "^3.8.0", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^2.67", + "nunomaduro/termwind": "^1.13", + "php": "^8.1", + "psr/container": "^1.1.1|^2.0.1", + "psr/log": "^1.0|^2.0|^3.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^6.2", + "symfony/error-handler": "^6.2", + "symfony/finder": "^6.2", + "symfony/http-foundation": "^6.4", + "symfony/http-kernel": "^6.2", + "symfony/mailer": "^6.2", + "symfony/mime": "^6.2", + "symfony/process": "^6.2", + "symfony/routing": "^6.2", + "symfony/uid": "^6.2", + "symfony/var-dumper": "^6.2", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.4.1", + "voku/portable-ascii": "^2.0" + }, + "conflict": { + "carbonphp/carbon-doctrine-types": ">=3.0", + "doctrine/dbal": ">=4.0", + "mockery/mockery": "1.6.8", + "phpunit/phpunit": ">=11.0.0", + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.235.5", + "doctrine/dbal": "^3.5.1", + "ext-gmp": "*", + "fakerphp/faker": "^1.21", + "guzzlehttp/guzzle": "^7.5", + "league/flysystem-aws-s3-v3": "^3.0", + "league/flysystem-ftp": "^3.0", + "league/flysystem-path-prefixing": "^3.3", + "league/flysystem-read-only": "^3.3", + "league/flysystem-sftp-v3": "^3.0", + "mockery/mockery": "^1.5.1", + "nyholm/psr7": "^1.2", + "orchestra/testbench-core": "^8.23.4", + "pda/pheanstalk": "^4.0", + "phpstan/phpstan": "~1.11.11", + "phpunit/phpunit": "^10.0.7", + "predis/predis": "^2.0.2", + "symfony/cache": "^6.2", + "symfony/http-client": "^6.2.4", + "symfony/psr-http-message-bridge": "^2.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.235.5).", + "brianium/paratest": "Required to run tests in parallel (^6.0).", + "doctrine/dbal": "Required to rename columns and drop SQLite columns (^3.5.1).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0).", + "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "guzzlehttp/guzzle": "Required to use the HTTP Client and the ping methods on schedules (^7.5).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.0).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.0).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.3).", + "league/flysystem-read-only": "Required to use read-only disks (^3.3)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.0).", + "mockery/mockery": "Required to use mocking (^1.5.1).", + "nyholm/psr7": "Required to use PSR-7 bridging features (^1.2).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).", + "phpunit/phpunit": "Required to use assertions and run tests (^9.5.8|^10.0.7).", + "predis/predis": "Required to use the predis connector (^2.0.2).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^6.2).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^6.2).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^6.2).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^6.2).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^6.2).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^2.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-12T14:42:01+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.1.25", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "7b4029a84c37cb2725fc7f011586e2997040bc95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/7b4029a84c37cb2725fc7f011586e2997040bc95", + "reference": "7b4029a84c37cb2725fc7f011586e2997040bc95", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/collections": "^10.0|^11.0", + "php": "^8.1", + "symfony/console": "^6.2|^7.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3", + "phpstan/phpstan": "^1.11", + "phpstan/phpstan-mockery": "^1.1" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.1.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.1.25" + }, + "time": "2024-08-12T22:06:33+00:00" + }, + { + "name": "laravel/sanctum", + "version": "v3.3.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "8c104366459739f3ada0e994bcd3e6fd681ce3d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/8c104366459739f3ada0e994bcd3e6fd681ce3d5", + "reference": "8c104366459739f3ada0e994bcd3e6fd681ce3d5", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^9.21|^10.0", + "illuminate/contracts": "^9.21|^10.0", + "illuminate/database": "^9.21|^10.0", + "illuminate/support": "^9.21|^10.0", + "php": "^8.0.2" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^7.28.2|^8.8.3", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.6" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2023-12-19T18:44:48+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v1.3.7", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "4f48ade902b94323ca3be7646db16209ec76be3d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/4f48ade902b94323ca3be7646db16209ec76be3d", + "reference": "4f48ade902b94323ca3be7646db16209ec76be3d", + "shasum": "" + }, + "require": { + "php": "^7.3|^8.0" + }, + "require-dev": { + "illuminate/support": "^8.0|^9.0|^10.0|^11.0", + "nesbot/carbon": "^2.61|^3.0", + "pestphp/pest": "^1.21.3", + "phpstan/phpstan": "^1.8.2", + "symfony/var-dumper": "^5.4.11|^6.2.0|^7.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2024-11-14T18:34:49+00:00" + }, + { + "name": "laravel/tinker", + "version": "v2.10.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/22177cc71807d38f2810c6204d8f7183d88a57d3", + "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3", + "shasum": "" + }, + "require": { + "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "php": "^7.2.5|^8.0", + "psy/psysh": "^0.11.1|^0.12.0", + "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v2.10.1" + }, + "time": "2025-01-27T14:24:01+00:00" + }, + { + "name": "lcobucci/clock", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/clock.git", + "reference": "fb533e093fd61321bfcbac08b131ce805fe183d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/fb533e093fd61321bfcbac08b131ce805fe183d3", + "reference": "fb533e093fd61321bfcbac08b131ce805fe183d3", + "shasum": "" + }, + "require": { + "php": "^8.0", + "stella-maris/clock": "^0.1.4" + }, + "require-dev": { + "infection/infection": "^0.26", + "lcobucci/coding-standard": "^8.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-deprecation-rules": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpstan/phpstan-strict-rules": "^0.12", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\Clock\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com" + } + ], + "description": "Yet another clock abstraction", + "support": { + "issues": "https://github.com/lcobucci/clock/issues", + "source": "https://github.com/lcobucci/clock/tree/2.2.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2022-04-19T19:34:17+00:00" + }, + { + "name": "lcobucci/jwt", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "55564265fddf810504110bd68ca311932324b0e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/55564265fddf810504110bd68ca311932324b0e9", + "reference": "55564265fddf810504110bd68ca311932324b0e9", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-openssl": "*", + "lcobucci/clock": "^2.0", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "infection/infection": "^0.20", + "lcobucci/coding-standard": "^6.0", + "mikey179/vfsstream": "^1.6", + "phpbench/phpbench": "^0.17", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-deprecation-rules": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpstan/phpstan-strict-rules": "^0.12", + "phpunit/php-invoker": "^3.1", + "phpunit/phpunit": "^9.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com", + "role": "Developer" + } + ], + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "keywords": [ + "JWS", + "jwt" + ], + "support": { + "issues": "https://github.com/lcobucci/jwt/issues", + "source": "https://github.com/lcobucci/jwt/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2021-09-28T19:18:28+00:00" + }, + { + "name": "league/commonmark", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "10732241927d3971d28e7ea7b5712721fa2296ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca", + "reference": "10732241927d3971d28e7ea7b5712721fa2296ca", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2025-07-20T12:47:49+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.30.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2203e3151755d874bb2943649dae1eb8533ac93e", + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.30.0" + }, + "time": "2025-06-25T13:29:59+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.30.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/6691915f77c7fb69adfb87dcd550052dc184ee10", + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.0" + }, + "time": "2025-05-21T10:34:19+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-09-21T08:32:55+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.9.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2025-03-24T10:02:05+00:00" + }, + { + "name": "nesbot/carbon", + "version": "2.73.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "9228ce90e1035ff2f0db84b40ec2e023ed802075" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/9228ce90e1035ff2f0db84b40ec2e023ed802075", + "reference": "9228ce90e1035ff2f0db84b40ec2e023ed802075", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "*", + "ext-json": "*", + "php": "^7.1.8 || ^8.0", + "psr/clock": "^1.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php80": "^1.16", + "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^2.0 || ^3.1.4 || ^4.0", + "doctrine/orm": "^2.7 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.0", + "kylekatarnls/multi-tester": "^2.0", + "ondrejmirtes/better-reflection": "<6", + "phpmd/phpmd": "^2.9", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12.99 || ^1.7.14", + "phpunit/php-file-iterator": "^2.0.5 || ^3.0.6", + "phpunit/phpunit": "^7.5.20 || ^8.5.26 || ^9.5.20", + "squizlabs/php_codesniffer": "^3.4" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbon.nesbot.com/docs", + "issues": "https://github.com/briannesbitt/Carbon/issues", + "source": "https://github.com/briannesbitt/Carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2025-01-08T20:10:23+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.2", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "da801d52f0354f70a638673c4a0f04e16529431d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", + "reference": "da801d52f0354f70a638673c4a0f04e16529431d", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.4" + }, + "require-dev": { + "nette/tester": "^2.5.2", + "phpstan/phpstan-nette": "^1.0", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.2" + }, + "time": "2024-10-06T23:10:23+00:00" + }, + { + "name": "nette/utils", + "version": "v4.0.7", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/e67c4061eb40b9c113b218214e42cb5a0dda28f2", + "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2", + "shasum": "" + }, + "require": { + "php": "8.0 - 8.4" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "^2.5", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.0.7" + }, + "time": "2025-06-03T04:55:08+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.6.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/221b0d0fdf1369c71047ad1d18bb5880017bbc56", + "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.0" + }, + "time": "2025-07-27T20:03:57+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v1.17.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "5369ef84d8142c1d87e4ec278711d4ece3cbf301" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/5369ef84d8142c1d87e4ec278711d4ece3cbf301", + "reference": "5369ef84d8142c1d87e4ec278711d4ece3cbf301", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.4.15" + }, + "require-dev": { + "illuminate/console": "^10.48.24", + "illuminate/support": "^10.48.24", + "laravel/pint": "^1.18.2", + "pestphp/pest": "^2.36.0", + "pestphp/pest-plugin-mock": "2.0.0", + "phpstan/phpstan": "^1.12.11", + "phpstan/phpstan-strict-rules": "^1.6.1", + "symfony/var-dumper": "^6.4.15", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Its like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v1.17.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2024-11-21T10:36:35+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.3", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54", + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:41:07+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.10", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/6e80abe6f2257121f1eb9a4c55bf29d921025b22", + "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2" + }, + "suggest": { + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-pdo-sqlite": "The doc command requires SQLite to work.", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.10" + }, + "time": "2025-08-04T12:39:37+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.0", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/4e0e23cc785f0724a0e838279a9eb03f28b092a0", + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.0" + }, + "time": "2025-06-25T14:20:11+00:00" + }, + { + "name": "stella-maris/clock", + "version": "0.1.7", + "source": { + "type": "git", + "url": "https://github.com/stella-maris-solutions/clock.git", + "reference": "fa23ce16019289a18bb3446fdecd45befcdd94f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stella-maris-solutions/clock/zipball/fa23ce16019289a18bb3446fdecd45befcdd94f8", + "reference": "fa23ce16019289a18bb3446fdecd45befcdd94f8", + "shasum": "" + }, + "require": { + "php": "^7.0|^8.0", + "psr/clock": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "StellaMaris\\Clock\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Heigl", + "role": "Maintainer" + } + ], + "description": "A pre-release of the proposed PSR-20 Clock-Interface", + "homepage": "https://gitlab.com/stella-maris/clock", + "keywords": [ + "clock", + "datetime", + "point in time", + "psr20" + ], + "support": { + "source": "https://github.com/stella-maris-solutions/clock/tree/0.1.7" + }, + "time": "2022-11-25T16:15:06+00:00" + }, + { + "name": "symfony/console", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "59266a5bf6a596e3e0844fd95e6ad7ea3c1d3350" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/59266a5bf6a596e3e0844fd95e6ad7ea3c1d3350", + "reference": "59266a5bf6a596e3e0844fd95e6ad7ea3c1d3350", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-30T10:38:54+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "30fd0b3cf0e972e82636038ce4db0e4fe777112c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/30fd0b3cf0e972e82636038ce4db0e4fe777112c", + "reference": "30fd0b3cf0e972e82636038ce4db0e4fe777112c", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/serializer": "^5.4|^6.0|^7.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-24T08:25:04+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "497f73ac996a598c92409b44ac43b6690c4f666d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/497f73ac996a598c92409b44ac43b6690c4f666d", + "reference": "497f73ac996a598c92409b44ac43b6690c4f666d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-22T09:11:45+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "73089124388c8510efb8d2d1689285d285937b08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/73089124388c8510efb8d2d1689285d285937b08", + "reference": "73089124388c8510efb8d2d1689285d285937b08", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T12:02:45+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "0341e41d8d8830c31a1dff5cbc5bdb3ec872a073" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/0341e41d8d8830c31a1dff5cbc5bdb3ec872a073", + "reference": "0341e41d8d8830c31a1dff5cbc5bdb3ec872a073", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:14:14+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "b81dcdbe34b8e8f7b3fc7b2a47fa065d5bf30726" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b81dcdbe34b8e8f7b3fc7b2a47fa065d5bf30726", + "reference": "b81dcdbe34b8e8f7b3fc7b2a47fa065d5bf30726", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<5.4", + "symfony/cache": "<5.4", + "symfony/config": "<6.1", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<5.4", + "symfony/form": "<5.4", + "symfony/http-client": "<5.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<5.4", + "symfony/messenger": "<5.4", + "symfony/translation": "<5.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<5.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.3", + "twig/twig": "<2.13" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^5.4|^6.0|^7.0", + "symfony/clock": "^6.2|^7.0", + "symfony/config": "^6.1|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/css-selector": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dom-crawler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4.5|^6.0.5|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4.4|^7.0.4", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4|^6.0|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.4|^7.0", + "symfony/var-exporter": "^6.2|^7.0", + "twig/twig": "^2.13|^3.0.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-31T09:23:30+00:00" + }, + { + "name": "symfony/mailer", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "b4d7fa2c69641109979ed06e98a588d245362062" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/b4d7fa2c69641109979ed06e98a588d245362062", + "reference": "b4d7fa2c69641109979ed06e98a588d245362062", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.1", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/mime": "^6.2|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4", + "symfony/messenger": "<6.2", + "symfony/mime": "<6.2", + "symfony/twig-bridge": "<6.2.1" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/messenger": "^6.2|^7.0", + "symfony/twig-bridge": "^6.2|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-24T08:25:04+00:00" + }, + { + "name": "symfony/mime", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "664d5e844a2de5e11c8255d0aef6bc15a9660ac7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/664d5e844a2de5e11c8255d0aef6bc15a9660ac7", + "reference": "664d5e844a2de5e11c8255d0aef6bc15a9660ac7", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<5.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.4|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T12:02:45+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/process", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "8eb6dc555bfb49b2703438d5de65cc9f138ff50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/8eb6dc555bfb49b2703438d5de65cc9f138ff50b", + "reference": "8eb6dc555bfb49b2703438d5de65cc9f138ff50b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:14:14+00:00" + }, + { + "name": "symfony/routing", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "e4f94e625c8e6f910aa004a0042f7b2d398278f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/e4f94e625c8e6f910aa004a0042f7b2d398278f5", + "reference": "e4f94e625c8e6f910aa004a0042f7b2d398278f5", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/annotations": "<1.12", + "symfony/config": "<6.2", + "symfony/dependency-injection": "<5.4", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.12|^2", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.2|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T08:46:37+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-25T09:37:31+00:00" + }, + { + "name": "symfony/string", + "version": "v7.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/42f505aff654e62ac7ac2ce21033818297ca89ca", + "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:47:49+00:00" + }, + { + "name": "symfony/translation", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "300b72643e89de0734d99a9e3f8494a3ef6936e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/300b72643e89de0734d99a9e3f8494a3ef6936e1", + "reference": "300b72643e89de0734d99a9e3f8494a3ef6936e1", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5|^3.0" + }, + "conflict": { + "symfony/config": "<5.4", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<5.4", + "symfony/yaml": "<5.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^4.18|^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-30T17:30:48+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-27T08:32:26+00:00" + }, + { + "name": "symfony/uid", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "17da16a750541a42cf2183935e0f6008316c23f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/17da16a750541a42cf2183935e0f6008316c23f7", + "reference": "17da16a750541a42cf2183935e0f6008316c23f7", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:14:14+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "aa29484ce0544bd69fa9f0df902e5ed7b7fe5034" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/aa29484ce0544bd69fa9f0df902e5ed7b7fe5034", + "reference": "aa29484ce0544bd69fa9f0df902e5ed7b7fe5034", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^6.3|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/uid": "^5.4|^6.0|^7.0", + "twig/twig": "^2.13|^3.0.4" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-29T18:40:01+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "0d72ac1c00084279c1816675284073c5a337c20d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", + "reference": "0d72ac1c00084279c1816675284073c5a337c20d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0" + }, + "time": "2024-12-21T16:25:41+00:00" + }, + { + "name": "tymon/jwt-auth", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/tymondesigns/jwt-auth.git", + "reference": "42381e56db1bf887c12e5302d11901d65cc74856" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tymondesigns/jwt-auth/zipball/42381e56db1bf887c12e5302d11901d65cc74856", + "reference": "42381e56db1bf887c12e5302d11901d65cc74856", + "shasum": "" + }, + "require": { + "illuminate/auth": "^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^9.0|^10.0|^11.0|^12.0", + "illuminate/http": "^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^9.0|^10.0|^11.0|^12.0", + "lcobucci/jwt": "^4.0", + "nesbot/carbon": "^2.69|^3.0", + "php": "^8.0" + }, + "require-dev": { + "illuminate/console": "^9.0|^10.0|^11.0|^12.0", + "illuminate/database": "^9.0|^10.0|^11.0|^12.0", + "illuminate/routing": "^9.0|^10.0|^11.0|^12.0", + "mockery/mockery": "^1.6", + "phpunit/phpunit": "^9.4" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "JWTAuth": "Tymon\\JWTAuth\\Facades\\JWTAuth", + "JWTFactory": "Tymon\\JWTAuth\\Facades\\JWTFactory" + }, + "providers": [ + "Tymon\\JWTAuth\\Providers\\LaravelServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.0-dev", + "dev-develop": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Tymon\\JWTAuth\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sean Tymon", + "email": "tymon148@gmail.com", + "homepage": "https://tymon.xyz", + "role": "Developer" + } + ], + "description": "JSON Web Token Authentication for Laravel and Lumen", + "homepage": "https://github.com/tymondesigns/jwt-auth", + "keywords": [ + "Authentication", + "JSON Web Token", + "auth", + "jwt", + "laravel" + ], + "support": { + "issues": "https://github.com/tymondesigns/jwt-auth/issues", + "source": "https://github.com/tymondesigns/jwt-auth" + }, + "funding": [ + { + "url": "https://www.patreon.com/seantymon", + "type": "patreon" + } + ], + "time": "2025-04-16T22:22:54+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.2", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.3", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-04-30T23:37:27+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2024-11-21T01:49:47+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" + } + ], + "packages-dev": [ + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.3", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "59a123a3d459c5a23055802237cb317f609867e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/59a123a3d459c5a23055802237cb317f609867e5", + "reference": "59a123a3d459c5a23055802237cb317f609867e5", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.3" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-06-16T00:02:10+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "laravel/breeze", + "version": "v1.29.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/breeze.git", + "reference": "22c53b84b7fff91b01a318d71a10dfc251e92849" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/breeze/zipball/22c53b84b7fff91b01a318d71a10dfc251e92849", + "reference": "22c53b84b7fff91b01a318d71a10dfc251e92849", + "shasum": "" + }, + "require": { + "illuminate/console": "^10.17", + "illuminate/filesystem": "^10.17", + "illuminate/support": "^10.17", + "illuminate/validation": "^10.17", + "php": "^8.1.0" + }, + "require-dev": { + "orchestra/testbench": "^8.0", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Breeze\\BreezeServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Breeze\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Minimal Laravel authentication scaffolding with Blade and Tailwind.", + "keywords": [ + "auth", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/breeze/issues", + "source": "https://github.com/laravel/breeze" + }, + "time": "2024-03-04T14:35:21+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.24.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/0345f3b05f136801af8c339f9d16ef29e6b4df8a", + "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.82.2", + "illuminate/view": "^11.45.1", + "larastan/larastan": "^3.5.0", + "laravel-zero/framework": "^11.45.0", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.3.1", + "pestphp/pest": "^2.36.0" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "files": [ + "overrides/Runner/Parallel/ProcessFactory.php" + ], + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2025-07-10T18:09:32+00:00" + }, + { + "name": "laravel/sail", + "version": "v1.44.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sail.git", + "reference": "a09097bd2a8a38e23ac472fa6a6cf5b0d1c1d3fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sail/zipball/a09097bd2a8a38e23ac472fa6a6cf5b0d1c1d3fe", + "reference": "a09097bd2a8a38e23ac472fa6a6cf5b0d1c1d3fe", + "shasum": "" + }, + "require": { + "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0", + "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0", + "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0", + "php": "^8.0", + "symfony/console": "^6.0|^7.0", + "symfony/yaml": "^6.0|^7.0" + }, + "require-dev": { + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", + "phpstan/phpstan": "^1.10" + }, + "bin": [ + "bin/sail" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sail\\SailServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Docker files for running a basic Laravel application.", + "keywords": [ + "docker", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/sail/issues", + "source": "https://github.com/laravel/sail" + }, + "time": "2025-07-04T16:17:06+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v7.12.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "995245421d3d7593a6960822063bdba4f5d7cf1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/995245421d3d7593a6960822063bdba4f5d7cf1a", + "reference": "995245421d3d7593a6960822063bdba4f5d7cf1a", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.17.0", + "nunomaduro/termwind": "^1.17.0", + "php": "^8.1.0", + "symfony/console": "^6.4.17" + }, + "conflict": { + "laravel/framework": ">=11.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.4.8", + "laravel/framework": "^10.48.29", + "laravel/pint": "^1.21.2", + "laravel/sail": "^1.41.0", + "laravel/sanctum": "^3.3.3", + "laravel/tinker": "^2.10.1", + "nunomaduro/larastan": "^2.10.0", + "orchestra/testbench-core": "^8.35.0", + "pestphp/pest": "^2.36.0", + "phpunit/phpunit": "^10.5.36", + "sebastian/environment": "^6.1.0", + "spatie/laravel-ignition": "^2.9.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2025-03-14T22:35:49+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:31:57+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.5.48", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "6e0a2bc39f6fae7617989d690d76c48e6d2eb541" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6e0a2bc39f6fae7617989d690d76c48e6d2eb541", + "reference": "6e0a2bc39f6fae7617989d690d76c48e6d2eb541", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.3", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.3", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.2", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.0", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.48" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-07-11T04:07:17+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:12:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-18T14:56:07+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68ff824baeae169ec9f2137158ee529584553799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:37:17+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-23T08:47:14+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "955288482d97c19a372d3f31006ab3f37da47adf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", + "reference": "955288482d97c19a372d3f31006ab3f37da47adf", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:17:12+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:19:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:38:20+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:05:40+00:00" + }, + { + "name": "sebastian/type", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" + }, + { + "name": "sebastian/version", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+00:00" + }, + { + "name": "spatie/backtrace", + "version": "1.7.4", + "source": { + "type": "git", + "url": "https://github.com/spatie/backtrace.git", + "reference": "cd37a49fce7137359ac30ecc44ef3e16404cccbe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/backtrace/zipball/cd37a49fce7137359ac30ecc44ef3e16404cccbe", + "reference": "cd37a49fce7137359ac30ecc44ef3e16404cccbe", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "ext-json": "*", + "laravel/serializable-closure": "^1.3 || ^2.0", + "phpunit/phpunit": "^9.3 || ^11.4.3", + "spatie/phpunit-snapshot-assertions": "^4.2 || ^5.1.6", + "symfony/var-dumper": "^5.1 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Backtrace\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van de Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "A better backtrace", + "homepage": "https://github.com/spatie/backtrace", + "keywords": [ + "Backtrace", + "spatie" + ], + "support": { + "source": "https://github.com/spatie/backtrace/tree/1.7.4" + }, + "funding": [ + { + "url": "https://github.com/sponsors/spatie", + "type": "github" + }, + { + "url": "https://spatie.be/open-source/support-us", + "type": "other" + } + ], + "time": "2025-05-08T15:41:09+00:00" + }, + { + "name": "spatie/error-solutions", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/spatie/error-solutions.git", + "reference": "e495d7178ca524f2dd0fe6a1d99a1e608e1c9936" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/error-solutions/zipball/e495d7178ca524f2dd0fe6a1d99a1e608e1c9936", + "reference": "e495d7178ca524f2dd0fe6a1d99a1e608e1c9936", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "illuminate/broadcasting": "^10.0|^11.0|^12.0", + "illuminate/cache": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "livewire/livewire": "^2.11|^3.5.20", + "openai-php/client": "^0.10.1", + "orchestra/testbench": "8.22.3|^9.0|^10.0", + "pestphp/pest": "^2.20|^3.0", + "phpstan/phpstan": "^2.1", + "psr/simple-cache": "^3.0", + "psr/simple-cache-implementation": "^3.0", + "spatie/ray": "^1.28", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "vlucas/phpdotenv": "^5.5" + }, + "suggest": { + "openai-php/client": "Require get solutions from OpenAI", + "simple-cache-implementation": "To cache solutions from OpenAI" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Ignition\\": "legacy/ignition", + "Spatie\\ErrorSolutions\\": "src", + "Spatie\\LaravelIgnition\\": "legacy/laravel-ignition" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "role": "Developer" + } + ], + "description": "This is my package error-solutions", + "homepage": "https://github.com/spatie/error-solutions", + "keywords": [ + "error-solutions", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/error-solutions/issues", + "source": "https://github.com/spatie/error-solutions/tree/1.1.3" + }, + "funding": [ + { + "url": "https://github.com/Spatie", + "type": "github" + } + ], + "time": "2025-02-14T12:29:50+00:00" + }, + { + "name": "spatie/flare-client-php", + "version": "1.10.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/flare-client-php.git", + "reference": "bf1716eb98bd689451b071548ae9e70738dce62f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/flare-client-php/zipball/bf1716eb98bd689451b071548ae9e70738dce62f", + "reference": "bf1716eb98bd689451b071548ae9e70738dce62f", + "shasum": "" + }, + "require": { + "illuminate/pipeline": "^8.0|^9.0|^10.0|^11.0|^12.0", + "php": "^8.0", + "spatie/backtrace": "^1.6.1", + "symfony/http-foundation": "^5.2|^6.0|^7.0", + "symfony/mime": "^5.2|^6.0|^7.0", + "symfony/process": "^5.2|^6.0|^7.0", + "symfony/var-dumper": "^5.2|^6.0|^7.0" + }, + "require-dev": { + "dms/phpunit-arraysubset-asserts": "^0.5.0", + "pestphp/pest": "^1.20|^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "spatie/pest-plugin-snapshots": "^1.0|^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\FlareClient\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Send PHP errors to Flare", + "homepage": "https://github.com/spatie/flare-client-php", + "keywords": [ + "exception", + "flare", + "reporting", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/flare-client-php/issues", + "source": "https://github.com/spatie/flare-client-php/tree/1.10.1" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-02-14T13:42:06+00:00" + }, + { + "name": "spatie/ignition", + "version": "1.15.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/ignition.git", + "reference": "31f314153020aee5af3537e507fef892ffbf8c85" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/ignition/zipball/31f314153020aee5af3537e507fef892ffbf8c85", + "reference": "31f314153020aee5af3537e507fef892ffbf8c85", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "php": "^8.0", + "spatie/error-solutions": "^1.0", + "spatie/flare-client-php": "^1.7", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "require-dev": { + "illuminate/cache": "^9.52|^10.0|^11.0|^12.0", + "mockery/mockery": "^1.4", + "pestphp/pest": "^1.20|^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "psr/simple-cache-implementation": "*", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "vlucas/phpdotenv": "^5.5" + }, + "suggest": { + "openai-php/client": "Require get solutions from OpenAI", + "simple-cache-implementation": "To cache solutions from OpenAI" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Spatie\\Ignition\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Spatie", + "email": "info@spatie.be", + "role": "Developer" + } + ], + "description": "A beautiful error page for PHP applications.", + "homepage": "https://flareapp.io/ignition", + "keywords": [ + "error", + "flare", + "laravel", + "page" + ], + "support": { + "docs": "https://flareapp.io/docs/ignition-for-laravel/introduction", + "forum": "https://twitter.com/flareappio", + "issues": "https://github.com/spatie/ignition/issues", + "source": "https://github.com/spatie/ignition" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-02-21T14:31:39+00:00" + }, + { + "name": "spatie/laravel-ignition", + "version": "2.9.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-ignition.git", + "reference": "1baee07216d6748ebd3a65ba97381b051838707a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/1baee07216d6748ebd3a65ba97381b051838707a", + "reference": "1baee07216d6748ebd3a65ba97381b051838707a", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "illuminate/support": "^10.0|^11.0|^12.0", + "php": "^8.1", + "spatie/ignition": "^1.15", + "symfony/console": "^6.2.3|^7.0", + "symfony/var-dumper": "^6.2.3|^7.0" + }, + "require-dev": { + "livewire/livewire": "^2.11|^3.3.5", + "mockery/mockery": "^1.5.1", + "openai-php/client": "^0.8.1|^0.10", + "orchestra/testbench": "8.22.3|^9.0|^10.0", + "pestphp/pest": "^2.34|^3.7", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan-deprecation-rules": "^1.1.1|^2.0", + "phpstan/phpstan-phpunit": "^1.3.16|^2.0", + "vlucas/phpdotenv": "^5.5" + }, + "suggest": { + "openai-php/client": "Require get solutions from OpenAI", + "psr/simple-cache-implementation": "Needed to cache solutions from OpenAI" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Flare": "Spatie\\LaravelIgnition\\Facades\\Flare" + }, + "providers": [ + "Spatie\\LaravelIgnition\\IgnitionServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\LaravelIgnition\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Spatie", + "email": "info@spatie.be", + "role": "Developer" + } + ], + "description": "A beautiful error page for Laravel applications.", + "homepage": "https://flareapp.io/ignition", + "keywords": [ + "error", + "flare", + "laravel", + "page" + ], + "support": { + "docs": "https://flareapp.io/docs/ignition-for-laravel/introduction", + "forum": "https://twitter.com/flareappio", + "issues": "https://github.com/spatie/laravel-ignition/issues", + "source": "https://github.com/spatie/laravel-ignition" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-02-20T13:13:55+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "b8d7d868da9eb0919e99c8830431ea087d6aae30" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/b8d7d868da9eb0919e99c8830431ea087d6aae30", + "reference": "b8d7d868da9eb0919e99c8830431ea087d6aae30", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:47:49+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.1" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/samooapk/laravel/config/app.php b/samooapk/laravel/config/app.php new file mode 100644 index 0000000..c303137 --- /dev/null +++ b/samooapk/laravel/config/app.php @@ -0,0 +1,188 @@ + 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 + | your application so that it is used when running Artisan tasks. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + 'asset_url' => env('ASSET_URL'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. We have gone + | ahead and set this to a sensible default for you out of the box. + | + */ + + 'timezone' => 'Asia/Jakarta', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by the translation service provider. You are free to set this value + | to any of the locales which will be supported by the application. + | + */ + + 'locale' => 'en', + + /* + |-------------------------------------------------------------------------- + | Application Fallback Locale + |-------------------------------------------------------------------------- + | + | The fallback locale determines the locale to use when the current one + | is not available. You may change the value to correspond to any of + | the language folders that are provided through your application. + | + */ + + 'fallback_locale' => 'en', + + /* + |-------------------------------------------------------------------------- + | Faker Locale + |-------------------------------------------------------------------------- + | + | This locale will be used by the Faker PHP library when generating fake + | data for your database seeds. For example, this will be used to get + | localized telephone numbers, street address information and more. + | + */ + + 'faker_locale' => 'en_US', + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is used by the Illuminate encrypter service and should be set + | to a random, 32 character string, otherwise these encrypted strings + | will not be safe. Please do this before deploying an application! + | + */ + + 'key' => env('APP_KEY'), + + 'cipher' => 'AES-256-CBC', + + /* + |-------------------------------------------------------------------------- + | 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' => 'file', + // 'store' => 'redis', + ], + + /* + |-------------------------------------------------------------------------- + | Autoloaded Service Providers + |-------------------------------------------------------------------------- + | + | The service providers listed here will be automatically loaded on the + | request to your application. Feel free to add your own services to + | this array to grant expanded functionality to your applications. + | + */ + + 'providers' => ServiceProvider::defaultProviders()->merge([ + /* + * Package Service Providers... + */ + + /* + * Application Service Providers... + */ + App\Providers\AppServiceProvider::class, + App\Providers\AuthServiceProvider::class, + // App\Providers\BroadcastServiceProvider::class, + App\Providers\EventServiceProvider::class, + App\Providers\RouteServiceProvider::class, + ])->toArray(), + + /* + |-------------------------------------------------------------------------- + | Class Aliases + |-------------------------------------------------------------------------- + | + | This array of class aliases will be registered when this application + | is started. However, feel free to register as many as you wish as + | the aliases are "lazy" loaded so they don't hinder performance. + | + */ + + 'aliases' => Facade::defaultAliases()->merge([ + // 'Example' => App\Facades\Example::class, + ])->toArray(), + +]; diff --git a/samooapk/laravel/config/auth.php b/samooapk/laravel/config/auth.php new file mode 100644 index 0000000..8ecf88a --- /dev/null +++ b/samooapk/laravel/config/auth.php @@ -0,0 +1,128 @@ + [ + 'guard' => 'web', + 'passwords' => 'users', + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | here which uses session storage and the Eloquent user provider. + | + | All authentication drivers have a user provider. This defines how the + | users are actually retrieved out of your database or other storage + | mechanisms used by this application to persist your user's data. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + + // Guard untuk API dengan JWT (digunakan untuk Teknisi Mobile App) + 'api' => [ + 'driver' => 'jwt', + 'provider' => 'akun_teknisi', + 'hash' => false, + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication drivers have a user provider. This defines how the + | users are actually retrieved out of your database or other storage + | mechanisms used by this application to persist your user's data. + | + | If you have multiple user tables or models you may configure multiple + | sources which represent each model / table. These sources may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => App\Models\User::class, + ], + + // Provider untuk Akun Teknisi (Mobile App) + 'akun_teknisi' => [ + 'driver' => 'eloquent', + 'model' => App\Models\AkunTeknisi::class, + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | You may specify multiple password reset configurations if you have more + | than one user table or model in the application and you want to have + | separate password reset settings based on the specific user types. + | + | 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' => 'password_reset_tokens', + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the amount of seconds before a password confirmation + | times out and the user is prompted to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => 10800, + +]; \ No newline at end of file diff --git a/samooapk/laravel/config/broadcasting.php b/samooapk/laravel/config/broadcasting.php new file mode 100644 index 0000000..2410485 --- /dev/null +++ b/samooapk/laravel/config/broadcasting.php @@ -0,0 +1,71 @@ + env('BROADCAST_DRIVER', 'null'), + + /* + |-------------------------------------------------------------------------- + | Broadcast Connections + |-------------------------------------------------------------------------- + | + | Here you may define all of the broadcast connections that will be used + | to broadcast events to other systems or over websockets. Samples of + | each available type of connection are provided inside this array. + | + */ + + 'connections' => [ + + 'pusher' => [ + 'driver' => 'pusher', + 'key' => env('PUSHER_APP_KEY'), + 'secret' => env('PUSHER_APP_SECRET'), + 'app_id' => env('PUSHER_APP_ID'), + 'options' => [ + 'cluster' => env('PUSHER_APP_CLUSTER'), + 'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com', + 'port' => env('PUSHER_PORT', 443), + 'scheme' => env('PUSHER_SCHEME', 'https'), + 'encrypted' => true, + 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https', + ], + 'client_options' => [ + // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html + ], + ], + + 'ably' => [ + 'driver' => 'ably', + 'key' => env('ABLY_KEY'), + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'default', + ], + + 'log' => [ + 'driver' => 'log', + ], + + 'null' => [ + 'driver' => 'null', + ], + + ], + +]; diff --git a/samooapk/laravel/config/cache.php b/samooapk/laravel/config/cache.php new file mode 100644 index 0000000..d4171e2 --- /dev/null +++ b/samooapk/laravel/config/cache.php @@ -0,0 +1,111 @@ + env('CACHE_DRIVER', 'file'), + + /* + |-------------------------------------------------------------------------- + | 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: "apc", "array", "database", "file", + | "memcached", "redis", "dynamodb", "octane", "null" + | + */ + + 'stores' => [ + + 'apc' => [ + 'driver' => 'apc', + ], + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'table' => 'cache', + 'connection' => null, + 'lock_connection' => null, + ], + + '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' => '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', + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, or 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(env('APP_NAME', 'laravel'), '_').'_cache_'), + +]; diff --git a/samooapk/laravel/config/cors.php b/samooapk/laravel/config/cors.php new file mode 100644 index 0000000..f2d44aa --- /dev/null +++ b/samooapk/laravel/config/cors.php @@ -0,0 +1,34 @@ + ['api/*', 'sanctum/csrf-cookie', 'storage/*'], + + 'allowed_methods' => ['*'], + + 'allowed_origins' => ['*'], + + 'allowed_origins_patterns' => [], + + 'allowed_headers' => ['*'], + + 'exposed_headers' => [], + + 'max_age' => 0, + + 'supports_credentials' => false, + +]; diff --git a/samooapk/laravel/config/database.php b/samooapk/laravel/config/database.php new file mode 100644 index 0000000..137ad18 --- /dev/null +++ b/samooapk/laravel/config/database.php @@ -0,0 +1,151 @@ + env('DB_CONNECTION', 'mysql'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Here are each of the database connections setup for your application. + | Of course, examples of configuring each database platform that is + | supported by Laravel is shown below to make development simple. + | + | + | All database work in Laravel is done through the PHP PDO facilities + | so make sure you have the driver for your particular database of + | choice installed on your machine before you begin development. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DATABASE_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8', + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => 'prefer', + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + '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 in the database. + | + */ + + 'migrations' => 'migrations', + + /* + |-------------------------------------------------------------------------- + | 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 APC or Memcached. Laravel makes it easy to dig right in. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), + ], + + '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'), + ], + + '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'), + ], + + ], + +]; diff --git a/samooapk/laravel/config/filesystems.php b/samooapk/laravel/config/filesystems.php new file mode 100644 index 0000000..e9d9dbd --- /dev/null +++ b/samooapk/laravel/config/filesystems.php @@ -0,0 +1,76 @@ + env('FILESYSTEM_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Here you may configure as many filesystem "disks" as you wish, and you + | may even configure multiple disks of the same driver. Defaults have + | been set up for each driver as an example of the required values. + | + | Supported Drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app'), + 'throw' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', + 'visibility' => 'public', + 'throw' => 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, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | 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'), + ], + +]; diff --git a/samooapk/laravel/config/hashing.php b/samooapk/laravel/config/hashing.php new file mode 100644 index 0000000..0e8a0bb --- /dev/null +++ b/samooapk/laravel/config/hashing.php @@ -0,0 +1,54 @@ + 'bcrypt', + + /* + |-------------------------------------------------------------------------- + | Bcrypt Options + |-------------------------------------------------------------------------- + | + | Here you may specify the configuration options that should be used when + | passwords are hashed using the Bcrypt algorithm. This will allow you + | to control the amount of time it takes to hash the given password. + | + */ + + 'bcrypt' => [ + 'rounds' => env('BCRYPT_ROUNDS', 12), + 'verify' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Argon Options + |-------------------------------------------------------------------------- + | + | Here you may specify the configuration options that should be used when + | passwords are hashed using the Argon algorithm. These will allow you + | to control the amount of time it takes to hash the given password. + | + */ + + 'argon' => [ + 'memory' => 65536, + 'threads' => 1, + 'time' => 4, + 'verify' => true, + ], + +]; diff --git a/samooapk/laravel/config/jwt.php b/samooapk/laravel/config/jwt.php new file mode 100644 index 0000000..f83234d --- /dev/null +++ b/samooapk/laravel/config/jwt.php @@ -0,0 +1,301 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return [ + + /* + |-------------------------------------------------------------------------- + | JWT Authentication Secret + |-------------------------------------------------------------------------- + | + | Don't forget to set this in your .env file, as it will be used to sign + | your tokens. A helper command is provided for this: + | `php artisan jwt:secret` + | + | Note: This will be used for Symmetric algorithms only (HMAC), + | since RSA and ECDSA use a private/public key combo (See below). + | + */ + + 'secret' => env('JWT_SECRET'), + + /* + |-------------------------------------------------------------------------- + | JWT Authentication Keys + |-------------------------------------------------------------------------- + | + | The algorithm you are using, will determine whether your tokens are + | signed with a random string (defined in `JWT_SECRET`) or using the + | following public & private keys. + | + | Symmetric Algorithms: + | HS256, HS384 & HS512 will use `JWT_SECRET`. + | + | Asymmetric Algorithms: + | RS256, RS384 & RS512 / ES256, ES384 & ES512 will use the keys below. + | + */ + + 'keys' => [ + + /* + |-------------------------------------------------------------------------- + | Public Key + |-------------------------------------------------------------------------- + | + | A path or resource to your public key. + | + | E.g. 'file://path/to/public/key' + | + */ + + 'public' => env('JWT_PUBLIC_KEY'), + + /* + |-------------------------------------------------------------------------- + | Private Key + |-------------------------------------------------------------------------- + | + | A path or resource to your private key. + | + | E.g. 'file://path/to/private/key' + | + */ + + 'private' => env('JWT_PRIVATE_KEY'), + + /* + |-------------------------------------------------------------------------- + | Passphrase + |-------------------------------------------------------------------------- + | + | The passphrase for your private key. Can be null if none set. + | + */ + + 'passphrase' => env('JWT_PASSPHRASE'), + + ], + + /* + |-------------------------------------------------------------------------- + | JWT time to live + |-------------------------------------------------------------------------- + | + | Specify the length of time (in minutes) that the token will be valid for. + | Defaults to 1 hour. + | + | You can also set this to null, to yield a never expiring token. + | Some people may want this behaviour for e.g. a mobile app. + | This is not particularly recommended, so make sure you have appropriate + | systems in place to revoke the token if necessary. + | Notice: If you set this to null you should remove 'exp' element from 'required_claims' list. + | + */ + + 'ttl' => env('JWT_TTL', 60), + + /* + |-------------------------------------------------------------------------- + | Refresh time to live + |-------------------------------------------------------------------------- + | + | Specify the length of time (in minutes) that the token can be refreshed + | within. I.E. The user can refresh their token within a 2 week window of + | the original token being created until they must re-authenticate. + | Defaults to 2 weeks. + | + | You can also set this to null, to yield an infinite refresh time. + | Some may want this instead of never expiring tokens for e.g. a mobile app. + | This is not particularly recommended, so make sure you have appropriate + | systems in place to revoke the token if necessary. + | + */ + + 'refresh_ttl' => env('JWT_REFRESH_TTL', 20160), + + /* + |-------------------------------------------------------------------------- + | JWT hashing algorithm + |-------------------------------------------------------------------------- + | + | Specify the hashing algorithm that will be used to sign the token. + | + */ + + 'algo' => env('JWT_ALGO', Tymon\JWTAuth\Providers\JWT\Provider::ALGO_HS256), + + /* + |-------------------------------------------------------------------------- + | Required Claims + |-------------------------------------------------------------------------- + | + | Specify the required claims that must exist in any token. + | A TokenInvalidException will be thrown if any of these claims are not + | present in the payload. + | + */ + + 'required_claims' => [ + 'iss', + 'iat', + 'exp', + 'nbf', + 'sub', + 'jti', + ], + + /* + |-------------------------------------------------------------------------- + | Persistent Claims + |-------------------------------------------------------------------------- + | + | Specify the claim keys to be persisted when refreshing a token. + | `sub` and `iat` will automatically be persisted, in + | addition to the these claims. + | + | Note: If a claim does not exist then it will be ignored. + | + */ + + 'persistent_claims' => [ + // 'foo', + // 'bar', + ], + + /* + |-------------------------------------------------------------------------- + | Lock Subject + |-------------------------------------------------------------------------- + | + | This will determine whether a `prv` claim is automatically added to + | the token. The purpose of this is to ensure that if you have multiple + | authentication models e.g. `App\User` & `App\OtherPerson`, then we + | should prevent one authentication request from impersonating another, + | if 2 tokens happen to have the same id across the 2 different models. + | + | Under specific circumstances, you may want to disable this behaviour + | e.g. if you only have one authentication model, then you would save + | a little on token size. + | + */ + + 'lock_subject' => true, + + /* + |-------------------------------------------------------------------------- + | Leeway + |-------------------------------------------------------------------------- + | + | This property gives the jwt timestamp claims some "leeway". + | Meaning that if you have any unavoidable slight clock skew on + | any of your servers then this will afford you some level of cushioning. + | + | This applies to the claims `iat`, `nbf` and `exp`. + | + | Specify in seconds - only if you know you need it. + | + */ + + 'leeway' => env('JWT_LEEWAY', 0), + + /* + |-------------------------------------------------------------------------- + | Blacklist Enabled + |-------------------------------------------------------------------------- + | + | In order to invalidate tokens, you must have the blacklist enabled. + | If you do not want or need this functionality, then set this to false. + | + */ + + 'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true), + + /* + | ------------------------------------------------------------------------- + | Blacklist Grace Period + | ------------------------------------------------------------------------- + | + | When multiple concurrent requests are made with the same JWT, + | it is possible that some of them fail, due to token regeneration + | on every request. + | + | Set grace period in seconds to prevent parallel request failure. + | + */ + + 'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 0), + + /* + |-------------------------------------------------------------------------- + | Cookies encryption + |-------------------------------------------------------------------------- + | + | By default Laravel encrypt cookies for security reason. + | If you decide to not decrypt cookies, you will have to configure Laravel + | to not encrypt your cookie token by adding its name into the $except + | array available in the middleware "EncryptCookies" provided by Laravel. + | see https://laravel.com/docs/master/responses#cookies-and-encryption + | for details. + | + | Set it to true if you want to decrypt cookies. + | + */ + + 'decrypt_cookies' => false, + + /* + |-------------------------------------------------------------------------- + | Providers + |-------------------------------------------------------------------------- + | + | Specify the various providers used throughout the package. + | + */ + + 'providers' => [ + + /* + |-------------------------------------------------------------------------- + | JWT Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to create and decode the tokens. + | + */ + + 'jwt' => Tymon\JWTAuth\Providers\JWT\Lcobucci::class, + + /* + |-------------------------------------------------------------------------- + | Authentication Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to authenticate users. + | + */ + + 'auth' => Tymon\JWTAuth\Providers\Auth\Illuminate::class, + + /* + |-------------------------------------------------------------------------- + | Storage Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to store tokens in the blacklist. + | + */ + + 'storage' => Tymon\JWTAuth\Providers\Storage\Illuminate::class, + + ], + +]; diff --git a/samooapk/laravel/config/logging.php b/samooapk/laravel/config/logging.php new file mode 100644 index 0000000..c44d276 --- /dev/null +++ b/samooapk/laravel/config/logging.php @@ -0,0 +1,131 @@ + 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' => false, + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Out of + | the box, Laravel uses the Monolog PHP logging library. This gives + | you a variety of powerful log handlers / formatters to utilize. + | + | Available Drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", + | "custom", "stack" + | + */ + + 'channels' => [ + 'stack' => [ + 'driver' => 'stack', + 'channels' => ['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' => 14, + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => 'Laravel Log', + '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, + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'with' => [ + 'stream' => 'php://stderr', + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + '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'), + ], + ], + +]; diff --git a/samooapk/laravel/config/mail.php b/samooapk/laravel/config/mail.php new file mode 100644 index 0000000..e894b2e --- /dev/null +++ b/samooapk/laravel/config/mail.php @@ -0,0 +1,134 @@ + env('MAIL_MAILER', 'smtp'), + + /* + |-------------------------------------------------------------------------- + | 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 to be used while + | sending an e-mail. You will specify which one you are using for your + | mailers below. You are free to add additional mailers as required. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "log", "array", "failover", "roundrobin" + | + */ + + 'mailers' => [ + 'smtp' => [ + 'transport' => 'smtp', + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), + 'port' => env('MAIL_PORT', 587), + 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN'), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => null, + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'mailgun' => [ + 'transport' => 'mailgun', + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + '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', + ], + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + ], + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all e-mails 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 e-mails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', 'Example'), + ], + + /* + |-------------------------------------------------------------------------- + | Markdown Mail Settings + |-------------------------------------------------------------------------- + | + | If you are using Markdown based email rendering, you may configure your + | theme and component paths here, allowing you to customize the design + | of the emails. Or, you may simply stick with the Laravel defaults! + | + */ + + 'markdown' => [ + 'theme' => 'default', + + 'paths' => [ + resource_path('views/vendor/mail'), + ], + ], + +]; diff --git a/samooapk/laravel/config/queue.php b/samooapk/laravel/config/queue.php new file mode 100644 index 0000000..01c6b05 --- /dev/null +++ b/samooapk/laravel/config/queue.php @@ -0,0 +1,109 @@ + env('QUEUE_CONNECTION', 'sync'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection information for each server that + | is used by your application. A default configuration has been added + | for each back-end shipped with Laravel. You are free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'table' => 'jobs', + 'queue' => 'default', + 'retry_after' => 90, + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => 'localhost', + 'queue' => 'default', + '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' => 'default', + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => 90, + 'block_for' => null, + 'after_commit' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | 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', 'mysql'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control which database and table are used to store the jobs that + | have failed. You may change them to any database / table you wish. + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'mysql'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/samooapk/laravel/config/sanctum.php b/samooapk/laravel/config/sanctum.php new file mode 100644 index 0000000..35d75b3 --- /dev/null +++ b/samooapk/laravel/config/sanctum.php @@ -0,0 +1,83 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort() + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of numerous + | security scanning initiatives maintained by open source platforms + | that notify developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, + 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, + 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, + ], + +]; diff --git a/samooapk/laravel/config/services.php b/samooapk/laravel/config/services.php new file mode 100644 index 0000000..0ace530 --- /dev/null +++ b/samooapk/laravel/config/services.php @@ -0,0 +1,34 @@ + [ + 'domain' => env('MAILGUN_DOMAIN'), + 'secret' => env('MAILGUN_SECRET'), + 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), + 'scheme' => 'https', + ], + + 'postmark' => [ + 'token' => env('POSTMARK_TOKEN'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + +]; diff --git a/samooapk/laravel/config/session.php b/samooapk/laravel/config/session.php new file mode 100644 index 0000000..e738cb3 --- /dev/null +++ b/samooapk/laravel/config/session.php @@ -0,0 +1,214 @@ + env('SESSION_DRIVER', 'file'), + + /* + |-------------------------------------------------------------------------- + | 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 immediately expire on the browser closing, set that option. + | + */ + + 'lifetime' => env('SESSION_LIFETIME', 120), + + 'expire_on_close' => false, + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it is stored. All encryption will be run + | automatically by Laravel and you can use the Session like normal. + | + */ + + 'encrypt' => false, + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When using the native session driver, we need a location where session + | files may be stored. A default has been set for you but a different + | location may be specified. This is only needed for file sessions. + | + */ + + '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 we + | should use to manage the sessions. Of course, a sensible default is + | provided for you; however, you are free to change this as needed. + | + */ + + 'table' => 'sessions', + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | While using one of the framework's cache driven session backends you may + | list a cache store that should be used for these sessions. This value + | must match with one of the application's configured cache "stores". + | + | Affects: "apc", "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 cookie used to identify a session + | instance by ID. The name specified here will get used every time a + | new session cookie is created by the framework for every driver. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug(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 are free to change this when necessary. + | + */ + + 'path' => '/', + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | Here you may change the domain of the cookie used to identify a session + | in your application. This will determine which domains the cookie is + | available to in your application. A sensible default has been set. + | + */ + + '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. You are free to modify this option if needed. + | + */ + + '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" since this is a secure default value. + | + | Supported: "lax", "strict", "none", null + | + */ + + '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' => false, + +]; diff --git a/samooapk/laravel/config/view.php b/samooapk/laravel/config/view.php new file mode 100644 index 0000000..22b8a18 --- /dev/null +++ b/samooapk/laravel/config/view.php @@ -0,0 +1,36 @@ + [ + resource_path('views'), + ], + + /* + |-------------------------------------------------------------------------- + | Compiled View Path + |-------------------------------------------------------------------------- + | + | This option determines where all the compiled Blade templates will be + | stored for your application. Typically, this is within the storage + | directory. However, as usual, you are free to change this value. + | + */ + + 'compiled' => env( + 'VIEW_COMPILED_PATH', + realpath(storage_path('framework/views')) + ), + +]; diff --git a/samooapk/laravel/database/.gitignore b/samooapk/laravel/database/.gitignore new file mode 100644 index 0000000..9b19b93 --- /dev/null +++ b/samooapk/laravel/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/samooapk/laravel/database/factories/UserFactory.php b/samooapk/laravel/database/factories/UserFactory.php new file mode 100644 index 0000000..584104c --- /dev/null +++ b/samooapk/laravel/database/factories/UserFactory.php @@ -0,0 +1,44 @@ + + */ +class UserFactory extends Factory +{ + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * Define the model's default state. + * + * @return array + */ + 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, + ]); + } +} diff --git a/samooapk/laravel/database/migrations/2014_10_12_000000_create_users_table.php b/samooapk/laravel/database/migrations/2014_10_12_000000_create_users_table.php new file mode 100644 index 0000000..444fafb --- /dev/null +++ b/samooapk/laravel/database/migrations/2014_10_12_000000_create_users_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + } +}; diff --git a/samooapk/laravel/database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php b/samooapk/laravel/database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php new file mode 100644 index 0000000..81a7229 --- /dev/null +++ b/samooapk/laravel/database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php @@ -0,0 +1,28 @@ +string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('password_reset_tokens'); + } +}; diff --git a/samooapk/laravel/database/migrations/2019_08_19_000000_create_failed_jobs_table.php b/samooapk/laravel/database/migrations/2019_08_19_000000_create_failed_jobs_table.php new file mode 100644 index 0000000..249da81 --- /dev/null +++ b/samooapk/laravel/database/migrations/2019_08_19_000000_create_failed_jobs_table.php @@ -0,0 +1,32 @@ +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('failed_jobs'); + } +}; diff --git a/samooapk/laravel/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php b/samooapk/laravel/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php new file mode 100644 index 0000000..e828ad8 --- /dev/null +++ b/samooapk/laravel/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->morphs('tokenable'); + $table->string('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/samooapk/laravel/database/migrations/2025_08_07_033606_create_teknisis_table.php b/samooapk/laravel/database/migrations/2025_08_07_033606_create_teknisis_table.php new file mode 100644 index 0000000..e9d51b4 --- /dev/null +++ b/samooapk/laravel/database/migrations/2025_08_07_033606_create_teknisis_table.php @@ -0,0 +1,34 @@ +id('id_teknisi'); + $table->string('nama', 100); + $table->date('tanggal_lahir'); + $table->text('alamat'); + $table->string('email', 100)->unique()->nullable(); + $table->string('no_telephone', 15); + $table->date('tanggal_masuk'); + $table->enum('status', ['aktif', 'tidak_aktif'])->default('aktif'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('teknisis'); + } +}; diff --git a/samooapk/laravel/database/migrations/2025_08_07_033645_create_penggajians_table.php b/samooapk/laravel/database/migrations/2025_08_07_033645_create_penggajians_table.php new file mode 100644 index 0000000..4640945 --- /dev/null +++ b/samooapk/laravel/database/migrations/2025_08_07_033645_create_penggajians_table.php @@ -0,0 +1,126 @@ +id('id_penggajian'); + + // =================================== + // IDENTITAS & PERIODE + // =================================== + $table->unsignedBigInteger('id_teknisi') + ->comment('ID teknisi yang digaji'); + + $table->integer('periode_bulan') + ->check('periode_bulan >= 1 AND periode_bulan <= 12') + ->comment('Bulan periode gaji (1-12)'); + + $table->year('periode_tahun') + ->comment('Tahun periode gaji'); + + $table->date('tanggal_penggajian') + ->comment('Tanggal gaji dihitung/dibuat'); + + // =================================== + // PENDAPATAN (HANYA ONGKOS PEKERJAAN) + // =================================== + $table->decimal('total_ongkos_pekerjaan', 12, 2) + ->default(0.00) + ->comment('Total ongkos dari penugasan yang selesai bulan ini (sudah dibagi tim)'); + + $table->integer('jumlah_penugasan_selesai') + ->default(0) + ->comment('Jumlah penugasan yang diselesaikan bulan ini'); + + $table->integer('jumlah_hari_kerja') + ->default(0) + ->comment('Jumlah hari kerja efektif bulan ini'); + + // =================================== + // POTONGAN (HANYA KASBON & BIAYA MAKAN) + // =================================== + $table->decimal('total_kasbon', 12, 2) + ->default(0.00) + ->comment('Total kasbon yang diambil bulan ini'); + + $table->decimal('biaya_makan', 12, 2) + ->default(0.00) + ->comment('Biaya makan yang dipotong dari gaji (hari kerja × tarif makan)'); + + $table->decimal('total_potongan', 12, 2) + ->default(0.00) + ->comment('Total potongan = kasbon + biaya_makan'); + + // =================================== + // GAJI BERSIH + // =================================== + $table->decimal('gaji_bersih', 12, 2) + ->default(0.00) + ->comment('Gaji bersih = ongkos_pekerjaan - total_potongan'); + + // =================================== + // STATUS & PEMBAYARAN + // =================================== + $table->enum('status_pembayaran', ['belum_bayar', 'sudah_bayar']) + ->default('belum_bayar') + ->comment('Status pembayaran gaji'); + + $table->enum('metode_pembayaran', ['cash', 'transfer']) + ->nullable() + ->comment('Metode pembayaran'); + + $table->dateTime('tanggal_dibayar') + ->nullable() + ->comment('Tanggal gaji benar-benar dibayarkan'); + + $table->text('bukti_pembayaran') + ->nullable() + ->comment('Path file/foto bukti transfer'); + + // =================================== + // CATATAN + // =================================== + $table->text('catatan') + ->nullable() + ->comment('Catatan tambahan untuk slip gaji ini'); + + $table->timestamps(); + + // =================================== + // FOREIGN KEYS + // =================================== + $table->foreign('id_teknisi') + ->references('id_teknisi') + ->on('teknisis') + ->onDelete('cascade'); + + // =================================== + // INDEXES + // =================================== + $table->unique(['id_teknisi', 'periode_bulan', 'periode_tahun'], 'unique_teknisi_periode'); + $table->index(['periode_tahun', 'periode_bulan'], 'idx_periode'); + $table->index('status_pembayaran', 'idx_status_bayar'); + $table->index('tanggal_penggajian', 'idx_tgl_gaji'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('penggajians'); + } +}; diff --git a/samooapk/laravel/database/migrations/2025_08_07_033736_create_tarif_pekerjaans_table.php b/samooapk/laravel/database/migrations/2025_08_07_033736_create_tarif_pekerjaans_table.php new file mode 100644 index 0000000..4441aee --- /dev/null +++ b/samooapk/laravel/database/migrations/2025_08_07_033736_create_tarif_pekerjaans_table.php @@ -0,0 +1,88 @@ +id('id_tarif'); + + // =================================== + // KATEGORI & JENIS PEKERJAAN + // =================================== + $table->enum('jenis_pekerjaan', [ + 'sr', // 1. Sambungan Rumah (meteran air) + 'pengembangan_jaringan_pipa', // 2. Pengembangan Jaringan Pipa (pipa baru) + 'pengangkatan', // 3. Pengangkatan meteran/gate valve + 'pemasangan_gate_valve', // 4. Pemasangan Gate Valve + Pressure + 'gali_urug', // 5. Gali Urug + 'perbaikan_jaringan_pipa', // 6. Perbaikan Jaringan Pipa Bocor + 'pengecatan_pipa_besi', // 7. Pengecatan Pipa Besi + 'penyempurnaan_jaringan_pipa' // 8. Penyempurnaan Jaringan Pipa + ])->comment('Jenis pekerjaan'); + + // =================================== + // IDENTITAS TARIF + // =================================== + $table->string('nama_item', 100)->comment('Nama item/pekerjaan'); + $table->string('kode_item', 20)->unique()->comment('Kode unik item'); + + // =================================== + // DIMENSI PIPA (untuk pekerjaan yang bergantung diameter) + // =================================== + $table->enum('dimensi_pipa', ['1-2', '3', '4', '6', '8', '10', '12']) + ->nullable() + ->comment('Dimensi/diameter pipa dalam inchi. NULL untuk pekerjaan yang tidak butuh dimensi pipa'); + + // =================================== + // TARIF PEKERJAAN + // =================================== + $table->decimal('tarif_per_unit', 12, 2) + ->nullable() + ->comment('Tarif flat per unit/titik. Dipakai untuk: SR (150k), Pengangkatan (200k), Gate Valve (200k/free), Gali Urug (200k/titik), Perbaikan & Pengecatan per dimensi'); + + $table->decimal('tarif_per_meter', 12, 2) + ->nullable() + ->comment('Tarif per meter. Dipakai untuk: Pengembangan & Penyempurnaan Jaringan Pipa sesuai dimensi'); + + // =================================== + // KONDISI KHUSUS + // =================================== + $table->boolean('pakai_pipa_besi') + ->nullable() + ->comment('Khusus Pemasangan Gate Valve: TRUE=ada pipa besi(200k), FALSE=tanpa pipa besi(free/0)'); + + // =================================== + // GARANSI (Khusus SR - Pemasangan Meteran Air) + // =================================== + $table->boolean('ada_garansi') + ->default(false) + ->comment('Apakah pekerjaan ini memiliki garansi?'); + + $table->integer('durasi_garansi_bulan') + ->nullable() + ->comment('Durasi garansi dalam bulan (3 bulan untuk SR)'); + + // =================================== + // INFO TAMBAHAN + // =================================== + $table->text('deskripsi')->nullable()->comment('Keterangan tambahan'); + + $table->boolean('is_active') + ->default(true) + ->comment('Status aktif tarif'); + + $table->timestamps(); + $table->softDeletes(); + }); + } + + public function down(): void + { + Schema::dropIfExists('tarif_pekerjaans'); + } +}; \ No newline at end of file diff --git a/samooapk/laravel/database/migrations/2025_08_07_033759_create_absensis_table.php b/samooapk/laravel/database/migrations/2025_08_07_033759_create_absensis_table.php new file mode 100644 index 0000000..11bf01a --- /dev/null +++ b/samooapk/laravel/database/migrations/2025_08_07_033759_create_absensis_table.php @@ -0,0 +1,39 @@ +id('id_absensi'); + $table->unsignedBigInteger('id_teknisi'); + $table->date('tanggal'); + $table->time('jam_masuk')->nullable(); + $table->time('jam_keluar')->nullable(); + $table->string('foto_absen_masuk', 255)->nullable(); + $table->string('foto_absen_keluar', 255)->nullable(); + $table->enum('status', ['hadir', 'izin', 'sakit', 'alpha']); + $table->text('keterangan')->nullable(); + $table->timestamps(); + + $table->foreign('id_teknisi')->references('id_teknisi')->on('teknisis')->onDelete('cascade'); + $table->unique(['id_teknisi', 'tanggal'], 'unique_teknisi_tanggal'); + $table->index('tanggal'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('absensis'); + } +}; diff --git a/samooapk/laravel/database/migrations/2025_08_07_033800_create_penugasans_table.php b/samooapk/laravel/database/migrations/2025_08_07_033800_create_penugasans_table.php new file mode 100644 index 0000000..87dcc3f --- /dev/null +++ b/samooapk/laravel/database/migrations/2025_08_07_033800_create_penugasans_table.php @@ -0,0 +1,126 @@ +id('id_penugasan'); + + // =================================== + // INFO PENUGASAN DASAR (Input Mandor) + // =================================== + $table->unsignedBigInteger('id_teknisi')->comment('Teknisi utama yang ditugaskan'); + $table->text('foto_surat')->nullable()->comment('Path foto surat tugas dari kantor'); // ✅ BARU + $table->date('tanggal_diberikan')->comment('Tanggal tugas diberikan'); + $table->text('catatan_admin')->nullable()->comment('Catatan/instruksi dari mandor'); + + // =================================== + // JENIS PEKERJAAN (Diisi Teknisi via Mobile) + // =================================== + $table->enum('jenis_pekerjaan', [ + 'sr', + 'pengembangan_jaringan_pipa', + 'pengangkatan', + 'pemasangan_gate_valve', + 'gali_urug', + 'perbaikan_jaringan_pipa', + 'pengecatan_pipa_besi', + 'penyempurnaan_jaringan_pipa' + ])->nullable()->comment('Jenis pekerjaan yang ditugaskan'); + + // =================================== + // DETAIL PEKERJAAN (Diisi Teknisi via Mobile) - SEMUA NULLABLE + // =================================== + $table->enum('dimensi_pipa', ['1-2', '3', '4', '6', '8', '10', '12']) + ->nullable() + ->comment('Dimensi pipa dalam inchi'); + + $table->decimal('jarak_meter', 10, 2) + ->nullable() + ->comment('Panjang pipa dalam meter'); + + $table->integer('jumlah_unit') + ->nullable() + ->comment('Jumlah unit. Untuk: SR, pengangkatan'); + + $table->integer('jumlah_titik') + ->nullable() + ->comment('Jumlah titik galian. Untuk: gali urug'); + + $table->boolean('pakai_pipa_besi') + ->nullable() + ->comment('Khusus pemasangan gate valve'); + + $table->enum('jenis_pengangkatan', ['meteran', 'gate_valve']) + ->nullable() + ->comment('Khusus pengangkatan: meteran air atau gate valve terpendam'); + + $table->text('detail_pekerjaan') + ->nullable() + ->comment('Catatan detail dari teknisi'); + + // =================================== + // ONGKOS PEKERJAAN + // =================================== + $table->unsignedBigInteger('id_tarif') + ->nullable() + ->comment('Referensi ke tabel tarif_pekerjaans'); + + $table->decimal('total_nilai_pekerjaan', 15, 2) + ->nullable() + ->comment('Total ongkos pekerjaan kotor (belum dibagi tim)'); + + // =================================== + // STATUS & TRACKING + // =================================== + $table->enum('status_pekerjaan', [ + 'belum_mulai', + 'dalam_proses', + 'selesai', + 'dibatalkan' + ])->default('belum_mulai'); + + $table->dateTime('tanggal_mulai')->nullable()->comment('Kapan teknisi mulai kerja'); + $table->dateTime('tanggal_diselesaikan')->nullable()->comment('Kapan pekerjaan selesai'); + + // =================================== + // GARANSI (Khusus SR - 3 bulan) + // =================================== + $table->date('tanggal_garansi_mulai')->nullable(); + $table->date('tanggal_garansi_selesai')->nullable(); + $table->text('catatan_garansi')->nullable(); + + // =================================== + // FOTO BUKTI PEKERJAAN (dari teknisi via mobile) + // =================================== + $table->text('foto_sebelum')->nullable()->comment('Path foto sebelum pekerjaan'); + $table->text('foto_sesudah')->nullable()->comment('Path foto setelah pekerjaan'); + + $table->timestamps(); + $table->softDeletes(); + + // =================================== + // FOREIGN KEYS + // =================================== + $table->foreign('id_teknisi') + ->references('id_teknisi') + ->on('teknisis') + ->onDelete('cascade'); + + $table->foreign('id_tarif') + ->references('id_tarif') + ->on('tarif_pekerjaans') + ->onDelete('set null'); + }); + } + + public function down(): void + { + Schema::dropIfExists('penugasans'); + } +}; \ No newline at end of file diff --git a/samooapk/laravel/database/migrations/2025_08_07_033844_create_kasbons_table.php b/samooapk/laravel/database/migrations/2025_08_07_033844_create_kasbons_table.php new file mode 100644 index 0000000..23ceb07 --- /dev/null +++ b/samooapk/laravel/database/migrations/2025_08_07_033844_create_kasbons_table.php @@ -0,0 +1,127 @@ +id('id_kasbon'); + + // =================================== + // IDENTITAS + // =================================== + $table->unsignedBigInteger('id_teknisi') + ->comment('ID teknisi yang ngasbon'); + + // =================================== + // DETAIL KASBON + // =================================== + $table->decimal('jumlah_kasbon', 12, 2) + ->comment('Jumlah uang yang dikasbon'); + + $table->date('tanggal_kasbon') + ->comment('Tanggal kasbon diambil'); + + $table->integer('periode_bulan') + ->comment('Bulan periode kasbon (1-12)') + ->check('periode_bulan >= 1 AND periode_bulan <= 12'); + + $table->year('periode_tahun') + ->comment('Tahun periode kasbon'); + + // =================================== + // KETERANGAN (OPSIONAL) + // =================================== + $table->string('keperluan', 100) + ->nullable() + ->comment('Keperluan kasbon (dropdown: biaya sekolah, obat, dll). OPSIONAL'); + + $table->text('keterangan_detail') + ->nullable() + ->comment('Keterangan tambahan jika perlu. OPSIONAL'); + + // =================================== + // PEMBERIAN KASBON (Oleh Mandor) + // =================================== + $table->enum('metode_pemberian', ['cash', 'transfer']) + ->default('cash') + ->comment('Metode pemberian kasbon oleh mandor'); + + // =================================== + // BUKTI KASBON (OPSIONAL) + // =================================== + $table->text('bukti_kasbon') + ->nullable() + ->comment('Path foto buku kasbon. OPSIONAL - bisa NULL untuk fase awal'); + + // =================================== + // TRACKING (Untuk Audit) + // =================================== + $table->unsignedBigInteger('diinput_oleh') + ->nullable() + ->comment('ID user/mandor yang input kasbon ini'); + + $table->dateTime('waktu_input') + ->default(DB::raw('CURRENT_TIMESTAMP')) + ->comment('Waktu kasbon diinput ke sistem (untuk audit)'); + + // =================================== + // STATUS & PELUNASAN + // =================================== + $table->enum('status', ['belum_lunas', 'lunas']) + ->default('belum_lunas') + ->comment('Status pelunasan kasbon'); + + $table->dateTime('tanggal_dilunasi') + ->nullable() + ->comment('Tanggal kasbon dilunasi (dipotong dari gaji)'); + + $table->unsignedBigInteger('id_penggajian') + ->nullable() + ->comment('ID penggajian yang memotong kasbon ini'); + + $table->text('catatan_pelunasan') + ->nullable() + ->comment('Catatan saat pelunasan'); + + $table->timestamps(); + + // =================================== + // FOREIGN KEYS + // =================================== + $table->foreign('id_teknisi') + ->references('id_teknisi') + ->on('teknisis') + ->onDelete('cascade'); + + $table->foreign('id_penggajian') + ->references('id_penggajian') + ->on('penggajians') + ->onDelete('set null'); + + // ✅ TAMBAHAN: Foreign key untuk diinput_oleh + $table->foreign('diinput_oleh') + ->references('id') + ->on('users') + ->onDelete('set null'); + + // =================================== + // INDEXES + // =================================== + $table->index('tanggal_kasbon', 'idx_tgl_kasbon'); + $table->index('status', 'idx_status'); + $table->index(['periode_tahun', 'periode_bulan'], 'idx_periode'); + $table->index('id_penggajian', 'idx_penggajian'); + }); + } + + public function down(): void + { + Schema::dropIfExists('kasbons'); + } +}; \ No newline at end of file diff --git a/samooapk/laravel/database/migrations/2025_08_07_033859_create_detail_penggajians_table.php b/samooapk/laravel/database/migrations/2025_08_07_033859_create_detail_penggajians_table.php new file mode 100644 index 0000000..fd70d25 --- /dev/null +++ b/samooapk/laravel/database/migrations/2025_08_07_033859_create_detail_penggajians_table.php @@ -0,0 +1,65 @@ +id('id_detail'); + + $table->unsignedBigInteger('id_penggajian') + ->comment('ID penggajian'); + + $table->unsignedBigInteger('id_penugasan') + ->comment('ID penugasan yang jadi sumber ongkos'); + + $table->date('tanggal_selesai') + ->comment('Tanggal penugasan selesai'); + + $table->string('lokasi') + ->comment('Lokasi pekerjaan'); + + $table->decimal('ongkos_penugasan', 12, 2) + ->comment('Total ongkos dari penugasan ini (kotor)'); + + $table->integer('jumlah_tim') + ->comment('Jumlah anggota tim yang hadir'); + + $table->decimal('bagian_ongkos', 12, 2) + ->comment('Bagian ongkos untuk teknisi ini (ongkos ÷ jumlah tim)'); + + $table->timestamps(); + + // Foreign Keys + $table->foreign('id_penggajian') + ->references('id_penggajian') + ->on('penggajians') + ->onDelete('cascade'); + + $table->foreign('id_penugasan') + ->references('id_penugasan') + ->on('penugasans') + ->onDelete('cascade'); + + // Index + $table->index('id_penggajian'); + $table->index('id_penugasan'); + }); + } + + public function down(): void + { + Schema::dropIfExists('detail_penggajians'); + } +}; \ No newline at end of file diff --git a/samooapk/laravel/database/migrations/2025_08_11_023136_create_akun_teknisis_table.php b/samooapk/laravel/database/migrations/2025_08_11_023136_create_akun_teknisis_table.php new file mode 100644 index 0000000..9b993e2 --- /dev/null +++ b/samooapk/laravel/database/migrations/2025_08_11_023136_create_akun_teknisis_table.php @@ -0,0 +1,34 @@ +id('id_akun_teknisi'); // Primary Key + $table->unsignedBigInteger('id_teknisi')->unique(); // Foreign Key ke tabel teknisis + $table->string('username')->unique(); + $table->string('password'); + $table->enum('status', ['aktif', 'tidak_aktif'])->default('aktif'); + $table->timestamps(); + + // Definisi Foreign Key Constraint + $table->foreign('id_teknisi')->references('id_teknisi')->on('teknisis')->onDelete('cascade'); + }); + } + + /** + * Kembalikan migrasi. + */ + public function down(): void + { + Schema::dropIfExists('akun_teknisis'); + } +}; \ No newline at end of file diff --git a/samooapk/laravel/database/migrations/2025_12_25_110336_create_tim_teknisi_penugasans_table.php b/samooapk/laravel/database/migrations/2025_12_25_110336_create_tim_teknisi_penugasans_table.php new file mode 100644 index 0000000..47ebe08 --- /dev/null +++ b/samooapk/laravel/database/migrations/2025_12_25_110336_create_tim_teknisi_penugasans_table.php @@ -0,0 +1,75 @@ +id('id_tim_penugasan'); + + // =================================== + // RELASI + // =================================== + $table->unsignedBigInteger('id_penugasan') + ->comment('ID penugasan yang dikerjakan'); + + $table->unsignedBigInteger('id_teknisi') + ->comment('ID teknisi anggota tim'); + + // =================================== + // STATUS KEHADIRAN + // Penting untuk perhitungan pembagian ongkos + // =================================== + $table->enum('status_kehadiran', ['hadir', 'tidak_hadir', 'izin']) + ->default('hadir') + ->comment('Status kehadiran teknisi. Hanya yang hadir dapat ongkos.'); + + // =================================== + // CATATAN + // =================================== + $table->text('catatan')->nullable() + ->comment('Catatan tambahan (misal: terlambat, pulang cepat, dll)'); + + $table->timestamps(); + + // =================================== + // FOREIGN KEYS + // =================================== + $table->foreign('id_penugasan') + ->references('id_penugasan') + ->on('penugasans') + ->onDelete('cascade'); + + $table->foreign('id_teknisi') + ->references('id_teknisi') + ->on('teknisis') + ->onDelete('cascade'); + + // =================================== + // UNIQUE CONSTRAINT + // Satu teknisi tidak bisa masuk 2x di penugasan yang sama + // =================================== + $table->unique(['id_penugasan', 'id_teknisi'], 'unique_penugasan_teknisi'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('tim_teknisi_penugasans'); + } +}; \ No newline at end of file diff --git a/samooapk/laravel/database/migrations/2026_04_24_125358_create_penugasan_items_table.php b/samooapk/laravel/database/migrations/2026_04_24_125358_create_penugasan_items_table.php new file mode 100644 index 0000000..97dd693 --- /dev/null +++ b/samooapk/laravel/database/migrations/2026_04_24_125358_create_penugasan_items_table.php @@ -0,0 +1,42 @@ +id('id_penugasan_item'); + $table->unsignedBigInteger('id_penugasan'); + $table->unsignedBigInteger('id_tarif')->nullable(); + + $table->string('jenis_pekerjaan'); + $table->string('dimensi_pipa')->nullable(); + $table->decimal('jarak_meter', 10, 2)->nullable(); + $table->integer('jumlah_unit')->nullable(); + $table->integer('jumlah_titik')->nullable(); + $table->boolean('pakai_pipa_besi')->nullable(); + $table->string('jenis_pengangkatan')->nullable(); // gate_valve atau meteran_air + + $table->decimal('total_nilai_pekerjaan', 15, 2)->default(0); + $table->timestamps(); + + $table->foreign('id_penugasan')->references('id_penugasan')->on('penugasans')->onDelete('cascade'); + $table->foreign('id_tarif')->references('id_tarif')->on('tarif_pekerjaans')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('penugasan_items'); + } +}; diff --git a/samooapk/laravel/database/migrations/2026_05_02_151800_add_structured_info_to_penugasans_table.php b/samooapk/laravel/database/migrations/2026_05_02_151800_add_structured_info_to_penugasans_table.php new file mode 100644 index 0000000..5eb52cc --- /dev/null +++ b/samooapk/laravel/database/migrations/2026_05_02_151800_add_structured_info_to_penugasans_table.php @@ -0,0 +1,24 @@ +string('alamat_lokasi')->nullable()->after('tanggal_diberikan'); + $table->string('nama_pelanggan')->nullable()->after('alamat_lokasi'); + $table->string('no_sambungan')->nullable()->after('nama_pelanggan'); + }); + } + + public function down(): void + { + Schema::table('penugasans', function (Blueprint $table) { + $table->dropColumn(['alamat_lokasi', 'nama_pelanggan', 'no_sambungan']); + }); + } +}; diff --git a/samooapk/laravel/database/migrations/2026_05_05_230156_add_rincian_pekerjaan_to_detail_penggajians_table.php b/samooapk/laravel/database/migrations/2026_05_05_230156_add_rincian_pekerjaan_to_detail_penggajians_table.php new file mode 100644 index 0000000..97af572 --- /dev/null +++ b/samooapk/laravel/database/migrations/2026_05_05_230156_add_rincian_pekerjaan_to_detail_penggajians_table.php @@ -0,0 +1,29 @@ +string('rincian_pekerjaan')->nullable()->after('bagian_ongkos') + ->comment('Rincian pekerjaan (misal: 10 Meter, 2 Unit)'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('detail_penggajians', function (Blueprint $table) { + $table->dropColumn('rincian_pekerjaan'); + }); + } +}; diff --git a/samooapk/laravel/database/migrations/2026_05_13_000001_add_password_plain_to_akun_teknisis.php b/samooapk/laravel/database/migrations/2026_05_13_000001_add_password_plain_to_akun_teknisis.php new file mode 100644 index 0000000..902c2da --- /dev/null +++ b/samooapk/laravel/database/migrations/2026_05_13_000001_add_password_plain_to_akun_teknisis.php @@ -0,0 +1,28 @@ +string('password_plain')->nullable()->after('password'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('akun_teknisis', function (Blueprint $table) { + $table->dropColumn('password_plain'); + }); + } +}; diff --git a/samooapk/laravel/database/migrations/2026_05_19_082612_add_location_to_absensis_table.php b/samooapk/laravel/database/migrations/2026_05_19_082612_add_location_to_absensis_table.php new file mode 100644 index 0000000..44cfe77 --- /dev/null +++ b/samooapk/laravel/database/migrations/2026_05_19_082612_add_location_to_absensis_table.php @@ -0,0 +1,30 @@ +string('latitude')->nullable()->after('foto_absen_keluar'); + $table->string('longitude')->nullable()->after('latitude'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('absensis', function (Blueprint $table) { + $table->dropColumn('latitude'); + $table->dropColumn('longitude'); + }); + } +}; diff --git a/samooapk/laravel/database/seeders/DatabaseSeeder.php b/samooapk/laravel/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..2cb2497 --- /dev/null +++ b/samooapk/laravel/database/seeders/DatabaseSeeder.php @@ -0,0 +1,19 @@ +call([ + // UserSeeder::class, + TarifPekerjaanSeeder::class, + ]); + } +} \ No newline at end of file diff --git a/samooapk/laravel/database/seeders/TarifPekerjaanSeeder.php b/samooapk/laravel/database/seeders/TarifPekerjaanSeeder.php new file mode 100644 index 0000000..ae00283 --- /dev/null +++ b/samooapk/laravel/database/seeders/TarifPekerjaanSeeder.php @@ -0,0 +1,486 @@ +insert([ + + // ========================================== + // 1. SR - Sambungan Rumah (Meteran Air) + // ========================================== + [ + 'jenis_pekerjaan' => 'sr', + 'nama_item' => 'Sambungan Rumah (SR) - Meteran Air', + 'kode_item' => 'SR-METERAN', + 'dimensi_pipa' => null, + 'tarif_per_unit' => 150000, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => true, + 'durasi_garansi_bulan' => 3, + 'deskripsi' => 'Pemasangan meteran air (SR) Rp 150.000 per unit, garansi 3 bulan', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + + // ========================================== + // 2. Pengembangan Jaringan Pipa (per meter, per dimensi) + // ========================================== + [ + 'jenis_pekerjaan' => 'pengembangan_jaringan_pipa', + 'nama_item' => 'Pengembangan Jaringan Pipa Dim 2', + 'kode_item' => 'PJP-DIM2', + 'dimensi_pipa' => '1-2', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 15000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pemasangan pipa baru diameter 2 dim, Rp 15.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pengembangan_jaringan_pipa', + 'nama_item' => 'Pengembangan Jaringan Pipa Dim 3', + 'kode_item' => 'PJP-DIM3', + 'dimensi_pipa' => '3', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 18000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pemasangan pipa baru diameter 3 dim, Rp 18.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pengembangan_jaringan_pipa', + 'nama_item' => 'Pengembangan Jaringan Pipa Dim 4', + 'kode_item' => 'PJP-DIM4', + 'dimensi_pipa' => '4', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 20000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pemasangan pipa baru diameter 4 dim, Rp 20.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pengembangan_jaringan_pipa', + 'nama_item' => 'Pengembangan Jaringan Pipa Dim 6', + 'kode_item' => 'PJP-DIM6', + 'dimensi_pipa' => '6', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 25000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pemasangan pipa baru diameter 6 dim, Rp 25.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pengembangan_jaringan_pipa', + 'nama_item' => 'Pengembangan Jaringan Pipa Dim 8', + 'kode_item' => 'PJP-DIM8', + 'dimensi_pipa' => '8', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 40000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pemasangan pipa baru diameter 8 dim, Rp 40.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pengembangan_jaringan_pipa', + 'nama_item' => 'Pengembangan Jaringan Pipa Dim 10', + 'kode_item' => 'PJP-DIM10', + 'dimensi_pipa' => '10', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 60000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pemasangan pipa baru diameter 10 dim, Rp 60.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + + // ========================================== + // 3. Pengangkatan (meteran atau gate valve) + // ========================================== + [ + 'jenis_pekerjaan' => 'pengangkatan', + 'nama_item' => 'Pengangkatan Meteran / Gate Valve', + 'kode_item' => 'ANGKAT-MGV', + 'dimensi_pipa' => null, + 'tarif_per_unit' => 200000, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pengangkatan meteran air atau gate valve terpendam, Rp 200.000 per unit', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + + // ========================================== + // 4. Pemasangan Gate Valve + Pressure + // ========================================== + [ + 'jenis_pekerjaan' => 'pemasangan_gate_valve', + 'nama_item' => 'Pemasangan Gate Valve + Pressure (Dengan Pipa Besi)', + 'kode_item' => 'GV-PAKAI-PIPA', + 'dimensi_pipa' => null, + 'tarif_per_unit' => 200000, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => true, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pemasangan gate valve + pressure dengan pipa besi, Rp 200.000', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pemasangan_gate_valve', + 'nama_item' => 'Pemasangan Gate Valve + Pressure (Tanpa Pipa Besi)', + 'kode_item' => 'GV-TANPA-PIPA', + 'dimensi_pipa' => null, + 'tarif_per_unit' => 0, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => false, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pemasangan gate valve + pressure tanpa pipa besi, GRATIS (Rp 0)', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + + // ========================================== + // 5. Gali Urug + // ========================================== + [ + 'jenis_pekerjaan' => 'gali_urug', + 'nama_item' => 'Gali Urug', + 'kode_item' => 'GALI-URUG', + 'dimensi_pipa' => null, + 'tarif_per_unit' => 200000, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Gali urug per titik, Rp 200.000/titik', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + + // ========================================== + // 6. Perbaikan Jaringan Pipa Bocor (per dimensi) + // ========================================== + [ + 'jenis_pekerjaan' => 'perbaikan_jaringan_pipa', + 'nama_item' => 'Perbaikan Pipa Bocor Dim 1-2', + 'kode_item' => 'PJP-BOR-DIM1-2', + 'dimensi_pipa' => '1-2', + 'tarif_per_unit' => 200000, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Perbaikan pipa bocor diameter 1-2 dim, Rp 200.000/titik', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'perbaikan_jaringan_pipa', + 'nama_item' => 'Perbaikan Pipa Bocor Dim 3', + 'kode_item' => 'PJP-BOR-DIM3', + 'dimensi_pipa' => '3', + 'tarif_per_unit' => 400000, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Perbaikan pipa bocor diameter 3 dim, Rp 400.000/titik', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'perbaikan_jaringan_pipa', + 'nama_item' => 'Perbaikan Pipa Bocor Dim 4', + 'kode_item' => 'PJP-BOR-DIM4', + 'dimensi_pipa' => '4', + 'tarif_per_unit' => 400000, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Perbaikan pipa bocor diameter 4 dim, Rp 400.000/titik', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'perbaikan_jaringan_pipa', + 'nama_item' => 'Perbaikan Pipa Bocor Dim 6', + 'kode_item' => 'PJP-BOR-DIM6', + 'dimensi_pipa' => '6', + 'tarif_per_unit' => 400000, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Perbaikan pipa bocor diameter 6 dim, Rp 400.000/titik', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'perbaikan_jaringan_pipa', + 'nama_item' => 'Perbaikan Pipa Bocor Dim 8', + 'kode_item' => 'PJP-BOR-DIM8', + 'dimensi_pipa' => '8', + 'tarif_per_unit' => 750000, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Perbaikan pipa bocor diameter 8 dim, Rp 750.000/titik', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'perbaikan_jaringan_pipa', + 'nama_item' => 'Perbaikan Pipa Bocor Dim 10', + 'kode_item' => 'PJP-BOR-DIM10', + 'dimensi_pipa' => '10', + 'tarif_per_unit' => 900000, + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Perbaikan pipa bocor diameter 10 dim, Rp 900.000/titik', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + + // ========================================== + // 7. Pengecatan Pipa Besi (2x tarif perbaikan per dimensi) + // ========================================== + [ + 'jenis_pekerjaan' => 'pengecatan_pipa_besi', + 'nama_item' => 'Pengecatan Pipa Besi Dim 1-2', + 'kode_item' => 'CAT-DIM1-2', + 'dimensi_pipa' => '1-2', + 'tarif_per_unit' => 400000, // 2x 200.000 + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pengecatan pipa besi dim 1-2, Rp 400.000 (2x tarif perbaikan)', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pengecatan_pipa_besi', + 'nama_item' => 'Pengecatan Pipa Besi Dim 3', + 'kode_item' => 'CAT-DIM3', + 'dimensi_pipa' => '3', + 'tarif_per_unit' => 800000, // 2x 400.000 + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pengecatan pipa besi dim 3, Rp 800.000 (2x tarif perbaikan)', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pengecatan_pipa_besi', + 'nama_item' => 'Pengecatan Pipa Besi Dim 4', + 'kode_item' => 'CAT-DIM4', + 'dimensi_pipa' => '4', + 'tarif_per_unit' => 800000, // 2x 400.000 + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pengecatan pipa besi dim 4, Rp 800.000 (2x tarif perbaikan)', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pengecatan_pipa_besi', + 'nama_item' => 'Pengecatan Pipa Besi Dim 6', + 'kode_item' => 'CAT-DIM6', + 'dimensi_pipa' => '6', + 'tarif_per_unit' => 800000, // 2x 400.000 + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pengecatan pipa besi dim 6, Rp 800.000 (2x tarif perbaikan)', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pengecatan_pipa_besi', + 'nama_item' => 'Pengecatan Pipa Besi Dim 8', + 'kode_item' => 'CAT-DIM8', + 'dimensi_pipa' => '8', + 'tarif_per_unit' => 1500000, // 2x 750.000 + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pengecatan pipa besi dim 8, Rp 1.500.000 (2x tarif perbaikan)', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'pengecatan_pipa_besi', + 'nama_item' => 'Pengecatan Pipa Besi Dim 10', + 'kode_item' => 'CAT-DIM10', + 'dimensi_pipa' => '10', + 'tarif_per_unit' => 1800000, // 2x 900.000 + 'tarif_per_meter' => null, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Pengecatan pipa besi dim 10, Rp 1.800.000 (2x tarif perbaikan)', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + + // ========================================== + // 8. Penyempurnaan Jaringan Pipa (sama seperti pengembangan) + // ========================================== + [ + 'jenis_pekerjaan' => 'penyempurnaan_jaringan_pipa', + 'nama_item' => 'Penyempurnaan Jaringan Pipa Dim 2', + 'kode_item' => 'SEMPURNA-DIM2', + 'dimensi_pipa' => '1-2', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 15000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Penyempurnaan jaringan pipa dim 2 (pipa kecil ke besar), Rp 15.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'penyempurnaan_jaringan_pipa', + 'nama_item' => 'Penyempurnaan Jaringan Pipa Dim 3', + 'kode_item' => 'SEMPURNA-DIM3', + 'dimensi_pipa' => '3', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 18000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Penyempurnaan jaringan pipa dim 3, Rp 18.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'penyempurnaan_jaringan_pipa', + 'nama_item' => 'Penyempurnaan Jaringan Pipa Dim 4', + 'kode_item' => 'SEMPURNA-DIM4', + 'dimensi_pipa' => '4', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 20000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Penyempurnaan jaringan pipa dim 4, Rp 20.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'penyempurnaan_jaringan_pipa', + 'nama_item' => 'Penyempurnaan Jaringan Pipa Dim 6', + 'kode_item' => 'SEMPURNA-DIM6', + 'dimensi_pipa' => '6', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 25000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Penyempurnaan jaringan pipa dim 6, Rp 25.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'penyempurnaan_jaringan_pipa', + 'nama_item' => 'Penyempurnaan Jaringan Pipa Dim 8', + 'kode_item' => 'SEMPURNA-DIM8', + 'dimensi_pipa' => '8', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 40000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Penyempurnaan jaringan pipa dim 8, Rp 40.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'jenis_pekerjaan' => 'penyempurnaan_jaringan_pipa', + 'nama_item' => 'Penyempurnaan Jaringan Pipa Dim 10', + 'kode_item' => 'SEMPURNA-DIM10', + 'dimensi_pipa' => '10', + 'tarif_per_unit' => null, + 'tarif_per_meter' => 60000, + 'pakai_pipa_besi' => null, + 'ada_garansi' => false, + 'durasi_garansi_bulan' => null, + 'deskripsi' => 'Penyempurnaan jaringan pipa dim 10, Rp 60.000/meter', + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ], + ]); + } +} \ No newline at end of file diff --git a/samooapk/laravel/package-lock.json b/samooapk/laravel/package-lock.json new file mode 100644 index 0000000..ccbf0ed --- /dev/null +++ b/samooapk/laravel/package-lock.json @@ -0,0 +1,2834 @@ +{ + "name": "samooapk", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@tailwindcss/forms": "^0.5.2", + "alpinejs": "^3.15.1", + "autoprefixer": "^10.4.2", + "axios": "^1.6.4", + "laravel-vite-plugin": "^1.0.0", + "postcss": "^8.4.31", + "tailwindcss": "^3.1.0", + "vite": "^5.0.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", + "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", + "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", + "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", + "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", + "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", + "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", + "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", + "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", + "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", + "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", + "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", + "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", + "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", + "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", + "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", + "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", + "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", + "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", + "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz", + "integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", + "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.1.5" + } + }, + "node_modules/@vue/shared": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", + "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/alpinejs": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.1.tgz", + "integrity": "sha512-HLO1TtiE92VajFHtLLPK8BWaK1YepV/uj31UrfoGnQ00lyFOJZ+oVY3F0DghPAwvg8sLU79pmjGQSytERa2gEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "~3.1.1" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.197", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.197.tgz", + "integrity": "sha512-m1xWB3g7vJ6asIFz+2pBUbq3uGmfmln1M9SSvBe4QIFWYrRHylP73zL/3nMjDmwz8V+1xAXQDfBd6+HPW0WvDQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/laravel-vite-plugin": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.3.0.tgz", + "integrity": "sha512-P5qyG56YbYxM8OuYmK2OkhcKe0AksNVJUjq9LUZ5tOekU9fBn9LujYyctI4t9XoLjuMvHJXXpCoPntY1oKltuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "vite-plugin-full-reload": "^1.1.0" + }, + "bin": { + "clean-orphaned-assets": "bin/clean.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "license": "MIT", + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", + "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.46.2", + "@rollup/rollup-android-arm64": "4.46.2", + "@rollup/rollup-darwin-arm64": "4.46.2", + "@rollup/rollup-darwin-x64": "4.46.2", + "@rollup/rollup-freebsd-arm64": "4.46.2", + "@rollup/rollup-freebsd-x64": "4.46.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", + "@rollup/rollup-linux-arm-musleabihf": "4.46.2", + "@rollup/rollup-linux-arm64-gnu": "4.46.2", + "@rollup/rollup-linux-arm64-musl": "4.46.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", + "@rollup/rollup-linux-ppc64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-musl": "4.46.2", + "@rollup/rollup-linux-s390x-gnu": "4.46.2", + "@rollup/rollup-linux-x64-gnu": "4.46.2", + "@rollup/rollup-linux-x64-musl": "4.46.2", + "@rollup/rollup-win32-arm64-msvc": "4.46.2", + "@rollup/rollup-win32-ia32-msvc": "4.46.2", + "@rollup/rollup-win32-x64-msvc": "4.46.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-full-reload": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz", + "integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "picomatch": "^2.3.1" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + } + } +} diff --git a/samooapk/laravel/package.json b/samooapk/laravel/package.json new file mode 100644 index 0000000..a6f428b --- /dev/null +++ b/samooapk/laravel/package.json @@ -0,0 +1,18 @@ +{ + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "devDependencies": { + "@tailwindcss/forms": "^0.5.2", + "alpinejs": "^3.15.1", + "autoprefixer": "^10.4.2", + "axios": "^1.6.4", + "laravel-vite-plugin": "^1.0.0", + "postcss": "^8.4.31", + "tailwindcss": "^3.1.0", + "vite": "^5.0.0" + } +} diff --git a/samooapk/laravel/phpunit.xml b/samooapk/laravel/phpunit.xml new file mode 100644 index 0000000..bc86714 --- /dev/null +++ b/samooapk/laravel/phpunit.xml @@ -0,0 +1,32 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + diff --git a/samooapk/laravel/postcss.config.js b/samooapk/laravel/postcss.config.js new file mode 100644 index 0000000..49c0612 --- /dev/null +++ b/samooapk/laravel/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/samooapk/laravel/resources/css/app.css b/samooapk/laravel/resources/css/app.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/samooapk/laravel/resources/css/app.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/samooapk/laravel/resources/js/app.js b/samooapk/laravel/resources/js/app.js new file mode 100644 index 0000000..a8093be --- /dev/null +++ b/samooapk/laravel/resources/js/app.js @@ -0,0 +1,7 @@ +import './bootstrap'; + +import Alpine from 'alpinejs'; + +window.Alpine = Alpine; + +Alpine.start(); diff --git a/samooapk/laravel/resources/js/bootstrap.js b/samooapk/laravel/resources/js/bootstrap.js new file mode 100644 index 0000000..846d350 --- /dev/null +++ b/samooapk/laravel/resources/js/bootstrap.js @@ -0,0 +1,32 @@ +/** + * We'll load the axios HTTP library which allows us to easily issue requests + * to our Laravel back-end. This library automatically handles sending the + * CSRF token as a header based on the value of the "XSRF" token cookie. + */ + +import axios from 'axios'; +window.axios = axios; + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; + +/** + * Echo exposes an expressive API for subscribing to channels and listening + * for events that are broadcast by Laravel. Echo and event broadcasting + * allows your team to easily build robust real-time web applications. + */ + +// import Echo from 'laravel-echo'; + +// import Pusher from 'pusher-js'; +// window.Pusher = Pusher; + +// window.Echo = new Echo({ +// broadcaster: 'pusher', +// key: import.meta.env.VITE_PUSHER_APP_KEY, +// cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1', +// wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`, +// wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80, +// wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443, +// forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https', +// enabledTransports: ['ws', 'wss'], +// }); diff --git a/samooapk/laravel/resources/views/Admin/Gaji/Kasbon.blade.php b/samooapk/laravel/resources/views/Admin/Gaji/Kasbon.blade.php new file mode 100644 index 0000000..e01c800 --- /dev/null +++ b/samooapk/laravel/resources/views/Admin/Gaji/Kasbon.blade.php @@ -0,0 +1,409 @@ + +@push('styles') + + +@endpush + +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+
Data Kasbon
+
Kelola pinjaman dan kasbon teknisi
+
+
+ +
+ + @if(session('success')) + + @endif + + {{-- ── DYNAMIC ALERT ── --}} + + + {{-- ── STATISTICS CARDS ── --}} +
+
+
+
TOTAL KASBON
+
{{ $totalKasbon ?? 0 }}
+
Semua catatan kasbon
+
+
+
+
LUNAS
+
{{ $kasbonLunas ?? 0 }}
+
Kasbon sudah diselesaikan
+
+
+
+
BELUM LUNAS
+
{{ $kasbonBelumLunas ?? 0 }}
+
Menunggu pelunasan
+
+
+
+
NOMINAL BELUM LUNAS
+
Rp {{ number_format($totalNominalBelumLunas ?? 0, 0, ',', '.') }}
+
Total hutang aktif
+
+
+
+
TOTAL NOMINAL
+
Rp {{ number_format($totalNominal ?? 0, 0, ',', '.') }}
+
Akumulasi seluruh kasbon
+
+
+ + {{-- ── TOOLBAR FILTER ── --}} +
+
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+
+ + {{-- ── DATA TABLE ── --}} +
+
+
+ + Daftar Kasbon +
+
+ Menampilkan {{ $kasbons->total() }} data kasbon +
+
+ +
+ + + + + + + + + + + + + + @forelse($kasbons as $index => $kasbon) + + + + + + + + + + @empty + + + + @endforelse + +
NoTeknisiTanggalJumlahStatusKeteranganAksi
+ {{ str_pad($kasbons->firstItem() + $index, 2, '0', STR_PAD_LEFT) }} + +
+
+ {{ strtoupper(substr($kasbon->teknisi->nama ?? 'T', 0, 1)) }} +
+
+
{{ $kasbon->teknisi->nama ?? 'Unknown' }}
+
+
+
+
{{ \Carbon\Carbon::parse($kasbon->tanggal_kasbon)->format('d/m/Y') }}
+
{{ \Carbon\Carbon::parse($kasbon->tanggal_kasbon)->isoFormat('dddd') }}
+
+
Rp {{ number_format($kasbon->jumlah_kasbon, 0, ',', '.') }}
+
+ @if($kasbon->status == 'lunas') + + Lunas + + @else + + Belum Lunas + + @endif + +
+ {{ $kasbon->keperluan ?? '-' }} +
+
+
+ + @if($kasbon->status == 'belum_lunas') + + @endif + +
+
+
+
+
Data Kasbon Tidak Ditemukan
+
Belum ada catatan kasbon untuk periode ini.
+
+
+
+ + @if($kasbons->hasPages()) +
+ {{ $kasbons->appends(request()->query())->links() }} +
+ @endif +
+
+
+ +{{-- ── MODAL TAMBAH/EDIT ── --}} +
+
+
+
Tambah Kasbon Baru
+ +
+ +
+ @csrf + + + +
+
+ + +
+ +
+ + +
+ +
+ +
+ Rp + +
+
+ + + +
+ + +
+
+ + +
+
+
+ +@push('scripts') + +@endpush +
\ No newline at end of file diff --git a/samooapk/laravel/resources/views/Admin/Gaji/Penggajian.blade.php b/samooapk/laravel/resources/views/Admin/Gaji/Penggajian.blade.php new file mode 100644 index 0000000..bebc716 --- /dev/null +++ b/samooapk/laravel/resources/views/Admin/Gaji/Penggajian.blade.php @@ -0,0 +1,547 @@ + +@push('styles') + +@endpush + +
+ + {{-- ALERT --}} + + + {{-- ── FORM PERIODE ── --}} +
+
+ + Pilih Periode Penggajian +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {{-- ── SUMMARY ── --}} + + + {{-- ── TABLE CARD ── --}} +
+
+
+ + Daftar Perhitungan Gaji +
+ +
+ + {{-- Loading --}} +
+
+
Sedang menghitung gaji teknisi...
+
+ + {{-- Empty state --}} +
+
💸
+
Belum ada data penggajian yang dimuat
+
Silakan pilih periode di atas lalu klik Hitung Gaji Otomatis
+
+ + {{-- Table --}} + +
+ +
+ +{{-- ── DETAIL MODAL ── --}} +
+
+
+
+ + Detail Perhitungan Gaji +
+ +
+
+
Memuat data…
+
+ +
+
+ +@push('scripts') + +@endpush +
\ No newline at end of file diff --git a/samooapk/laravel/resources/views/Admin/Gaji/detail_penggajian.blade.php b/samooapk/laravel/resources/views/Admin/Gaji/detail_penggajian.blade.php new file mode 100644 index 0000000..90ada67 --- /dev/null +++ b/samooapk/laravel/resources/views/Admin/Gaji/detail_penggajian.blade.php @@ -0,0 +1,137 @@ +
+
+
+
+ +
+
+
Slip Gaji Teknisi
+
+ Periode: {{ \App\Models\Penggajian::getNamaBulan($penggajian->periode_bulan) }} {{ $penggajian->periode_tahun }} +
+
+
+
+
Status
+ @if($penggajian->isPaid()) + LUNAS + @else + BELUM DIBAYAR + @endif +
+
+ +
+ +
+
Data Personel
+
+
+ Nama Lengkap + {{ $penggajian->teknisi->nama }} +
+
+ ID Teknisi + #{{ str_pad($penggajian->id_teknisi, 4, '0', STR_PAD_LEFT) }} +
+
+ Tgl. Hitung + {{ $penggajian->tanggal_penggajian->format('d M Y') }} +
+
+ Absensi Hadir + {{ $penggajian->jumlah_hari_kerja }} Hari +
+
+
+ + +
+
Summary Borongan
+
+
+ Jumlah Tugas + {{ $penggajian->detailPenggajian->count() }} Tugas +
+
+ Gaji Kotor + Rp {{ number_format($penggajian->total_ongkos_pekerjaan, 0, ',', '.') }} +
+
+ Potongan Makan +
+ - Rp {{ number_format($penggajian->biaya_makan, 0, ',', '.') }} + @if(!$penggajian->isPaid()) + + @endif +
+
+
+ Potongan Kasbon +
+ - Rp {{ number_format($penggajian->total_potongan ?? $penggajian->total_kasbon, 0, ',', '.') }} + @if(!$penggajian->isPaid()) + + @endif +
+
+
+
+
+ + +
+
+ Rincian Pembagian Ongkos Penugasan +
+
+ + + + + + + + + + @foreach($penggajian->detailPenggajian as $item) + + + + + + @endforeach + +
Tugas / LokasiTimBagian Anda
+
{{ $item->penugasan->label_jenis_pekerjaan ?? 'Tugas #'.$item->id_penugasan }}
+
{{ \Illuminate\Support\Str::limit($item->lokasi, 40) }}
+ @if($item->rincian_pekerjaan) +
+ {{ $item->rincian_pekerjaan }} +
+ @endif +
+ {{ $item->jumlah_tim }} Org + + Rp {{ number_format($item->bagian_ongkos, 0, ',', '.') }} +
+
+
+ + +
+
+
Take Home Pay
+
Total Gaji Bersih Terakhir
+
+
+
+ Rp {{ number_format($penggajian->gaji_bersih, 0, ',', '.') }} +
+
+
+
diff --git a/samooapk/laravel/resources/views/Admin/Gaji/slip_penggajian.blade.php b/samooapk/laravel/resources/views/Admin/Gaji/slip_penggajian.blade.php new file mode 100644 index 0000000..938a79d --- /dev/null +++ b/samooapk/laravel/resources/views/Admin/Gaji/slip_penggajian.blade.php @@ -0,0 +1,207 @@ + + + + + + Slip Gaji - {{ $penggajian->teknisi->nama }} + + + +
+
+

Slip Gaji Teknisi

+

PDAM Tirta Sanjiwani

+

Periode: {{ \Carbon\Carbon::create()->month($penggajian->periode_bulan)->translatedFormat('F') }} {{ $penggajian->periode_tahun }}

+
+ +
+ + + + +
Nama Teknisi:{{ $penggajian->teknisi->nama }}
ID / NIK:{{ $penggajian->teknisi->id_teknisi }}
Jabatan:{{ $penggajian->teknisi->spesialisasi ?? 'Teknisi Lapangan' }}
+ + + + +
No. Slip:SLP-{{ $penggajian->periode_tahun }}{{ str_pad($penggajian->periode_bulan, 2, '0', STR_PAD_LEFT) }}-{{ str_pad($penggajian->id_penggajian, 4, '0', STR_PAD_LEFT) }}
Tgl Cetak:{{ $penggajian->tanggal_penggajian->format('d/m/Y') }}
Status:{{ $penggajian->status_pembayaran == 'sudah_bayar' ? 'LUNAS' : 'BELUM DIBAYAR' }}
+
+ + + + + + + + + + + + + @if($penggajian->detailPenggajian && $penggajian->detailPenggajian->count() > 0) + @foreach($penggajian->detailPenggajian as $index => $item) + + + + + + + + @endforeach + @else + + + + @endif + +
NoRincian Penugasan SelesaiOngkos TugasTimBagian (Rp)
{{ $index + 1 }} + {{ $item->penugasan->label_jenis_pekerjaan ?? 'Tugas #'.$item->id_penugasan }}
+ {{ $item->lokasi }} + @if($item->rincian_pekerjaan) +
{{ $item->rincian_pekerjaan }} + @endif +
Rp {{ number_format($item->ongkos_penugasan, 0, ',', '.') }}{{ $item->jumlah_tim }}Rp {{ number_format($item->bagian_ongkos, 0, ',', '.') }}
Tidak ada data penugasan di bulan ini.
+ + + + + + + + + + + + + + + + + + +
Total Ongkos Pekerjaan{{ number_format($penggajian->total_ongkos_pekerjaan, 0, ',', '.') }}
Uang Makan ({{ $penggajian->jumlah_hari_kerja }} Hari Hadir)+ {{ number_format($penggajian->biaya_makan, 0, ',', '.') }}
Potongan Kasbon Berjalan- {{ number_format($penggajian->total_potongan ?? $penggajian->total_kasbon, 0, ',', '.') }}
Gaji Bersih DiterimaRp {{ number_format($penggajian->gaji_bersih, 0, ',', '.') }}
+ +
+
+

Mengetahui/Menyetujui,
Manajer Operasional

+
PDAM Tirta Sanjiwani
+
+
+

Diterima Oleh,
Teknisi Lapangan

+
{{ $penggajian->teknisi->nama }}
+
+
+
+ + diff --git a/samooapk/laravel/resources/views/Admin/KelolaPekerjaan/Penugasan.blade.php b/samooapk/laravel/resources/views/Admin/KelolaPekerjaan/Penugasan.blade.php new file mode 100644 index 0000000..134a347 --- /dev/null +++ b/samooapk/laravel/resources/views/Admin/KelolaPekerjaan/Penugasan.blade.php @@ -0,0 +1,809 @@ + + +@push('styles') + +@endpush +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+
Data Penugasan
+
Manajemen penugasan teknisi lapangan
+
+
+ +
+ + +
+
+ + @if(session('success')) + + @endif + @if(session('error')) + + @endif + + {{-- ── ABANDONED TASKS ALERT ── --}} + @if(isset($abandonedTasks) && $abandonedTasks->count() > 0) +
+
+ +
+
+

PERHATIAN: Ada Personel Tim yang Sakit/Izin Hari Ini!

+

+ Terdapat {{ $abandonedTasks->count() }} Penugasan (Belum Mulai / Dalam Proses) yang anggotanya hari ini berstatus Sakit / Izin. Segera cek dan sesuaikan formasi tim berikut: +

+
+ @foreach($abandonedTasks as $task) +
+ {{ \Illuminate\Support\Str::limit($task->jenis_pekerjaan, 20) }} + {{ \Illuminate\Support\Str::limit($task->alamat_lokasi, 25) }} + +
+ @endforeach +
+
+
+ @endif + + +
+
+
+
Total
+
{{ $totalPenugasan ?? 0 }}
+
Semua penugasan
+
+
+
+
Belum Mulai
+
{{ $belumMulai ?? 0 }}
+
Menunggu eksekusi
+
+
+
+
Dalam Proses
+
{{ $dalamProses ?? 0 }}
+
Sedang dikerjakan
+
+
+
+
Selesai
+
{{ $selesai ?? 0 }}
+
Pekerjaan rampung
+
+
+
+
Dibatalkan
+
{{ $dibatalkan ?? 0 }}
+
Penugasan dibatalkan
+
+
+
+
Garansi Aktif
+
{{ $garansiAktif ?? 0 }}
+
Dalam periode garansi
+
+
+ + +
+ + +
+
+ + +
+ +
+ + S/D + +
+
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+
+
+ + +
+
+
+ + Daftar Penugasan +
+
+ {{ isset($penugasan) ? $penugasan->total() : 0 }} data ditemukan +
+
+ +
+ + + + + + + + + + + + + @forelse($penugasan ?? [] as $index => $item) + + + + + + + + + + + @empty + + + + @endforelse + +
#TeknisiSuratTanggalStatus PekerjaanAksi
+ {{ str_pad($penugasan->firstItem() + $index, 2, '0', STR_PAD_LEFT) }} + +
+ @foreach($item->timTeknisi as $tt) + + {{ $tt->teknisi->nama ?? 'N/A' }} + + @endforeach +
+
+ @if($item->foto_surat) + Surat + @else + N/A + @endif + +
{{ \Carbon\Carbon::parse($item->tanggal_diberikan)->format('d/m/Y') }}
+
+ @php + $bMap = [ + 'belum_mulai' => ['class'=>'pug-badge-muted', 'icon'=>'fa-clock', 'label'=>'Belum Mulai'], + 'dalam_proses'=> ['class'=>'pug-badge-violet', 'icon'=>'fa-spinner', 'label'=>'Dalam Proses'], + 'selesai' => ['class'=>'pug-badge-green', 'icon'=>'fa-check-circle', 'label'=>'Selesai'], + 'dibatalkan' => ['class'=>'pug-badge-rose', 'icon'=>'fa-times-circle', 'label'=>'Dibatalkan'], + ]; + $bCfg = $bMap[$item->status_pekerjaan] ?? $bMap['belum_mulai']; + @endphp + + {{ $bCfg['label'] }} + + +
+ + + +
+
+
+
+
Belum ada penugasan
+
Klik "Tambah Penugasan" untuk membuat penugasan baru
+
+
+
+ + @if(isset($penugasan) && $penugasan->hasPages()) +
{{ $penugasan->links() }}
+ @endif +
+ +
+
+
+ + +
+
+
+
Tambah Penugasan Baru
+ +
+ +
+ +

Metode Foto: Silakan unggah foto daftar tugas/surat tugas dari kantor. Teknisi akan melihat foto ini di aplikasi mereka.

+
+ +
+ @csrf + + + +
+ +
+ + +
+
+
+ +
+ +
+ @foreach($teknisiList ?? [] as $teknisi) +
+ {{ $teknisi->nama }} + {{ $teknisi->spesialisasi }} +
+ @endforeach +
Tidak ada hasil
+
+
+ + +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+ +
+ +

Klik untuk unggah denah lokasi atau surat tugas

+ Format: JPG, PNG, WEBP (Boleh dikosongkan) +
+ +
+
+ + +
+ + +
+
+ + +
+
+
+ + +
+
+
+
Detail Penugasan
+ +
+
+
+
+ + +
+ Preview + +
+ + + \ No newline at end of file diff --git a/samooapk/laravel/resources/views/Admin/KelolaPekerjaan/ProgresKerja.blade.php b/samooapk/laravel/resources/views/Admin/KelolaPekerjaan/ProgresKerja.blade.php new file mode 100644 index 0000000..9a0dd7d --- /dev/null +++ b/samooapk/laravel/resources/views/Admin/KelolaPekerjaan/ProgresKerja.blade.php @@ -0,0 +1,314 @@ + +@push('styles') + + + +@endpush + +
+
+ +
+
+
+ +
+
+

Monitoring Progres Kerja

+

Pantau real-time perkembangan tugas teknisi di lapangan.

+
+
+
+ + +
+
+
+
TOTAL TUGAS
+
{{ $statistics['total'] ?? 0 }}
+
+
+
+
DALAM PROGRES
+
{{ $statistics['dalam_progres'] ?? 0 }}
+
+
+
+
SELESAI
+
{{ $statistics['selesai'] ?? 0 }}
+
+
+
+
BELUM MULAI
+
{{ $statistics['belum_mulai'] ?? 0 }}
+
+
+ +
+ +
+
+ +
+ + +
+
+ +
+ + s/d + +
+
+ +
+
+ + +
+
+
+ DAFTAR PROGRES KERJA +
+
+ Menampilkan {{ count($progresKerja) }} data terbaru +
+
+ +
+ + + + + + + + + + + + + @forelse($progresKerja as $index => $progres) + + + + + + + + + @empty + + + + @endforelse + +
#Teknisi / TimFotoPekerjaan & LokasiStatusAksi
+ {{ str_pad($progresKerja->firstItem() + $index, 2, '0', STR_PAD_LEFT) }} + +
+ @foreach($progres->timTeknisi as $member) + + {{ $member->teknisi->nama }} + + @endforeach +
+
+ @php + $fotoTampil = $progres->foto_sesudah ?? $progres->foto_sebelum ?? $progres->foto_surat; + @endphp + @if($fotoTampil) + + @else +
+ +
+ @endif +
+
{{ $progres->label_jenis_pekerjaan }}
+
{{ Str::limit($progres->alamat_lokasi, 40) }}
+
+ @switch($progres->status_pekerjaan) + @case('belum_mulai') Belum Mulai @break + @case('dalam_proses') Progres @break + @case('selesai') Selesai @break + @endswitch +
{{ \Carbon\Carbon::parse($progres->updated_at)->format('d/m H:i') }}
+
+
+ +
+
+
+
+
Data Tidak Ditemukan
+
Gunakan filter untuk mencari data progres lainnya.
+
+
+
+ + @if($progresKerja->hasPages()) +
+ {{ $progresKerja->links() }} +
+ @endif +
+
+
+
+ + +
+
+
+
+ DETAIL PROGRES KERJA +
+ +
+
+ +
+
+

Mengambil data...

+
+
+
+
+ + +
+ + +
+ +@push('scripts') + + +@endpush +
\ No newline at end of file diff --git a/samooapk/laravel/resources/views/Admin/KelolaTeknisi/Absensi.blade.php b/samooapk/laravel/resources/views/Admin/KelolaTeknisi/Absensi.blade.php new file mode 100644 index 0000000..b148148 --- /dev/null +++ b/samooapk/laravel/resources/views/Admin/KelolaTeknisi/Absensi.blade.php @@ -0,0 +1,407 @@ + +@push('styles') + + + +@endpush + +
+
+ {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+

Monitoring Absensi

+

Kelola & monitor kehadiran teknisi lapangan

+
+
+
+ + {{-- ── STATISTICS CARDS ── --}} +
+
+
+
TOTAL ABSENSI
+
{{ $counts['total'] ?? 0 }}
+
Semua teknisi terdaftar
+
+
+
+
HADIR
+
{{ $counts['hadir'] ?? 0 }}
+
Teknisi yang sudah masuk
+
+
+
+
IZIN / SAKIT
+
{{ $counts['izin'] ?? 0 }}
+
Izin berhalangan kerja
+
+
+ + +
+ +
+
+ +
+ + s/d + +
+
+ +
+ +
+ + +
+
+ +
+ +
+
+
+ + {{-- ── PANEL TABLE ── --}} +
+
+
+ DAFTAR LIST ABSENSI +
+
+ Menampilkan {{ $absensis->total() }} data absensi +
+
+ +
+ + + + + + + + + + + + + + + @forelse($absensis as $index => $abs) + + + + + + + + + + + @empty + + + + @endforelse + +
#TeknisiTanggalJam MasukJam KeluarDurasiStatusAksi
+ {{ str_pad($absensis->firstItem() + $index, 2, '0', STR_PAD_LEFT) }} + +
+
+ {{ strtoupper(substr($abs->teknisi->nama ?? 'T', 0, 1)) }} +
+
+
{{ $abs->teknisi->nama ?? 'Unknown' }}
+
{{ $abs->teknisi->email ?? '—' }}
+
+
+
+
{{ \Carbon\Carbon::parse($abs->tanggal)->format('d/m/Y') }}
+
{{ \Carbon\Carbon::parse($abs->tanggal)->isoFormat('dddd') }}
+
+ @if($abs->jam_masuk) +
{{ \Carbon\Carbon::parse($abs->jam_masuk)->format('H:i') }}
+ @php + $kat = $abs->kategori_kerja; + $badgeClass = $kat == 'Kerja Urgent' ? 'badge-urgent' : 'badge-normal'; + $icon = $kat == 'Kerja Urgent' ? 'exclamation-triangle' : 'sun'; + @endphp +
+ {{ $kat }} +
+ @else + + @endif +
+ {{ $abs->jam_keluar ? \Carbon\Carbon::parse($abs->jam_keluar)->format('H:i') : '—' }} + + {{ $abs->durasi_kerja_formatted ?? '—' }} + + @php + $s = strtolower($abs->status); + $badgeClass = 'pug-badge-green'; + $icon = 'check-circle'; + if($s == 'izin' || $s == 'sakit') { $badgeClass = 'pug-badge-violet'; $icon = 'info-circle'; } + @endphp + + {{ ucfirst($abs->status) }} + + +
+ @if(!empty($abs->latitude) && !empty($abs->longitude) && $abs->latitude !== '0' && $abs->longitude !== '0') + + + + @endif + + +
+
+
+
+
Data Absensi Tidak Ditemukan
+
Gunakan filter untuk mencari data di tanggal lain.
+
+
+
+ @if($absensis->hasPages()) +
+ {{ $absensis->appends(request()->query())->links() }} +
+ @endif +
+
+
+ + +
+
+
+
DETAIL ABSENSI TEKNISI
+ +
+
+
+
+ + +
+
+
+
EDIT DATA ABSENSI
+ +
+
+ @csrf @method('PUT') + +
+
+ +

+

+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
\ No newline at end of file diff --git a/samooapk/laravel/resources/views/Admin/KelolaTeknisi/AkunTeknisi.blade.php b/samooapk/laravel/resources/views/Admin/KelolaTeknisi/AkunTeknisi.blade.php new file mode 100644 index 0000000..c34360d --- /dev/null +++ b/samooapk/laravel/resources/views/Admin/KelolaTeknisi/AkunTeknisi.blade.php @@ -0,0 +1,510 @@ + + +@push('styles') + + + +@endpush + +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+

Kelola Akun Teknisi

+

Manajemen akun & akses aplikasi teknisi lapangan

+
+
+ +
+ + {{-- ── STATISTICS CARDS ── --}} +
+
+
+
TOTAL AKUN
+
{{ $akunTeknisis->count() }}
+
Semua akun terdaftar
+
+
+
+
AKUN AKTIF
+
{{ $akunTeknisis->where('status','aktif')->count() }}
+
Memiliki akses aplikasi
+
+
+
+
NONAKTIF
+
{{ $akunTeknisis->where('status','tidak_aktif')->count() }}
+
Akses akun dicabut
+
+
+ + {{-- ── ALERT ── --}} + + + {{-- ── TOOLBAR ── --}} +
+
+
+ +
+ + +
+
+ +
+
+ + {{-- ── PANEL TABLE ── --}} +
+
+
+ DAFTAR AKUN TEKNISI +
+
+ Menampilkan {{ $akunTeknisis->count() }} akun terdaftar +
+
+ +
+ + + + + + + + + + + + @forelse($akunTeknisis as $index => $akun) + + + + + + + + @empty + + + + @endforelse + + + + +
#Nama TeknisiUsernameStatus AkunAksi
+ {{ str_pad($index + 1, 2, '0', STR_PAD_LEFT) }} + +
+
+ {{ strtoupper(substr($akun->teknisi->nama ?? 'T', 0, 1)) }} +
+
+
{{ $akun->teknisi->nama ?? '—' }}
+
Teknisi Lapangan
+
+
+
+ + {{ $akun->username }} + + +
+ @if($akun->status == 'aktif') + + Aktif + + @else + + Nonaktif + + @endif + +
+
+
+ + + +
+
+
+
+
Belum Ada Akun Teknisi
+
Silakan tambahkan akun untuk akses aplikasi teknisi.
+
+
+
+
+ +{{-- ── MODAL DETAIL AKUN ── --}} +
+
+
+
DETAIL AKUN TEKNISI
+ +
+
+
+
+

+
+
+ +
+
+ +
+ + +
+
+ +
+ +
+ + +
+

* Gunakan fitur 'Edit' untuk mereset password jika lupa.

+
+ +
+ +
+ +
+
+
ID Akun
+
+
+
+
ID Teknisi
+
+
+
+
Terdaftar Sejak
+
+
+
+
+
+
+ +
+
+ +
+
+ +{{-- ── MODAL TAMBAH ── --}} +
+
+
+
TAMBAH AKUN TEKNISI
+ +
+
+ @csrf +
+
+ + + +
+
+ + + +
+
+ +
+ + +
+ +
+
+ + +
+
+ +
+
+
+ +{{-- ── MODAL EDIT ── --}} +
+
+
+
EDIT AKUN TEKNISI
+ +
+
+ @csrf @method('PUT') + + +
+
+ + +
+
+ + + +
+
+ +
+ + +
+
+
+ + +
+
+ +
+
+
+ + + +
\ No newline at end of file diff --git a/samooapk/laravel/resources/views/Admin/KelolaTeknisi/Teknisi.blade.php b/samooapk/laravel/resources/views/Admin/KelolaTeknisi/Teknisi.blade.php new file mode 100644 index 0000000..878a449 --- /dev/null +++ b/samooapk/laravel/resources/views/Admin/KelolaTeknisi/Teknisi.blade.php @@ -0,0 +1,417 @@ + + +@push('styles') + + + +@endpush + +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+

Data Teknisi

+

Kelola & monitor profil lengkap teknisi lapangan

+
+
+ +
+ + {{-- ── STATISTICS CARDS ── --}} +
+
+
+
TOTAL TEKNISI
+
0
+
Semua teknisi terdaftar
+
+
+
+
TEKNISI AKTIF
+
0
+
Sedang aktif bertugas
+
+
+
+
TIDAK AKTIF
+
0
+
Tidak sedang bertugas
+
+
+ + {{-- ── TOOLBAR (SEARCH) ── --}} +
+
+
+ +
+ + +
+
+
+ +
+
+
+ + {{-- ── PANEL TABLE ── --}} +
+
+
+ DAFTAR INFORMASI TEKNISI +
+
Memuat data…
+
+ +
+ + + + + + + + + + + + + + + + +
#Nama TeknisiNo. TeleponTgl MasukStatusAksi
+
+
+
Memuat Data...
+
+
+
+ +
+
+ + Menampilkan 00 dari 0 teknisi +
+
+
+ +
+
+ +{{-- ── MODAL DETAIL TEKNISI ── --}} +
+
+
+
Detail Profil Teknisi
+ +
+
+
+
+

+

+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ +
+
+
+ +
+
+
+
+
+ +
+
+ +{{-- ── MODAL TAMBAH / EDIT ── --}} + + + + +
\ No newline at end of file diff --git a/samooapk/laravel/resources/views/Admin/Laporan.blade.php b/samooapk/laravel/resources/views/Admin/Laporan.blade.php new file mode 100644 index 0000000..04c7a2d --- /dev/null +++ b/samooapk/laravel/resources/views/Admin/Laporan.blade.php @@ -0,0 +1,257 @@ + +@push('styles') + + +@endpush + +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+

Laporan Sistem

+

Statistik dan rincian operasional PDAM • {{ date('F Y') }}

+
+
+
+ + {{-- ── TAB NAVIGATION ── --}} + + + {{-- ── STATS ── --}} +
+
+
+
TOTAL TENAGA KERJA
+
{{ $statistics['total_teknisi'] ?? 0 }}
+
{{ $statistics['teknisi_aktif'] ?? 0 }} aktif • {{ $statistics['teknisi_nonaktif'] ?? 0 }} nonaktif
+
+
+
+
PEKERJAAN BULAN INI
+
{{ $statistics['selesai'] ?? 0 }}
+
{{ $statistics['progress'] ?? 0 }} proses • {{ $statistics['pending'] ?? 0 }} pending
+
+
+
+
KASBON BELUM LUNAS
+
Rp {{ number_format($statistics['total_belum_lunas'] ?? 0, 0, ',', '.') }}
+
Dari {{ $statistics['total_kasbon'] ?? 0 }} total kasbon
+
+
+
+
KEHADIRAN BULAN INI
+
{{ $statistics['hadir'] ?? 0 }}
+
{{ $statistics['izin'] ?? 0 }} izin • {{ $statistics['sakit'] ?? 0 }} sakit • {{ $statistics['alpha'] ?? 0 }} alpha
+
+
+ + {{-- ── TABLES ── --}} + + +
+
+
+ + Kasbon Terbaru +
+ +
+
+ + + + + + + + + + + + @forelse($recentKasbon as $item) + + + + + + + + @empty + + + + @endforelse + +
TeknisiTanggalJumlahStatusKeperluan
+
+
{{ strtoupper(substr($item->nama_teknisi ?? '?', 0, 2)) }}
+
{{ $item->nama_teknisi ?? '-' }}
+
+
{{ \Carbon\Carbon::parse($item->tanggal_kasbon)->format('d M Y') }}
Rp {{ number_format($item->jumlah_kasbon, 0, ',', '.') }}
+ + {{ strtoupper($item->status) }} + + {{ $item->keperluan ?? '-' }}
+
+
Tidak ada data kasbon
+
+
+
+
+ +
+ +
+
+
+ + Absensi Terbaru +
+
+ Semua +
+
+
+ + + + + + + + + + @forelse($recentAbsensi as $item) + + + + + + @empty + + @endforelse + +
TeknisiTanggalStatus
+
+
{{ strtoupper(substr($item->nama_teknisi ?? '?', 0, 2)) }}
+
{{ $item->nama_teknisi ?? '-' }}
+
+
{{ \Carbon\Carbon::parse($item->tanggal)->format('d M Y') }}
+ @php + $st = strtolower($item->status); + $bc = 'pug-badge-amber'; $ic = 'fa-clock'; + if($st == 'hadir') { $bc = 'pug-badge-green'; $ic = 'fa-check-circle'; } + if(in_array($st, ['alpha','sakit'])) { $bc = 'pug-badge-rose'; $ic = 'fa-times-circle'; } + if($st == 'izin') { $bc = 'pug-badge-violet'; $ic = 'fa-info-circle'; } + @endphp + + {{ strtoupper($item->status) }} + +
Tidak ada data
+
+
+ + +
+
+
+ + Pekerjaan Terbaru +
+
+ Semua +
+
+
+ + + + + + + + + + @forelse($recentPekerjaan as $item) + + + + + + @empty + + @endforelse + +
PekerjaanTeknisiStatus
+
+
{{ $item->jenis_pekerjaan ?? '-' }}
+
#{{ $item->id_penugasan }}
+
+
+
+
{{ strtoupper(substr($item->nama_teknisi ?? '?', 0, 2)) }}
+
{{ $item->nama_teknisi ?? '-' }}
+
+
+ @php + $sp = strtolower($item->status_pekerjaan ?? 'pending'); + $bcp = 'pug-badge-muted'; $icp = 'fa-clock'; + if($sp == 'selesai') { $bcp = 'pug-badge-green'; $icp = 'fa-check-circle'; } + if($sp == 'proses' || $sp == 'dalam_proses') { $bcp = 'pug-badge-violet'; $icp = 'fa-spinner'; } + @endphp + + {{ strtoupper(str_replace('_', ' ', $item->status_pekerjaan ?? '-')) }} + +
Tidak ada data
+
+
+
+ +
+
+
\ No newline at end of file diff --git a/samooapk/laravel/resources/views/Admin/Laporan/absensi.blade.php b/samooapk/laravel/resources/views/Admin/Laporan/absensi.blade.php new file mode 100644 index 0000000..630b628 --- /dev/null +++ b/samooapk/laravel/resources/views/Admin/Laporan/absensi.blade.php @@ -0,0 +1,196 @@ + +@push('styles') + + +@endpush + +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+

Laporan Kehadiran

+

Rekapitulasi kehadiran harian teknisi PDAM • {{ date('F Y') }}

+
+
+ + Cetak Laporan + +
+ + {{-- ── TAB NAVIGATION ── --}} + + + {{-- ── STATS ── --}} +
+
+
+
HADIR BULAN INI
+
{{ $statsAbsensi['hadir'] ?? 0 }}
+
Kehadiran tercatat
+
+
+
+
IZIN
+
{{ $statsAbsensi['izin'] ?? 0 }}
+
Permohonan izin
+
+
+
+
SAKIT
+
{{ $statsAbsensi['sakit'] ?? 0 }}
+
Keterangan sakit
+
+
+
+
ALPHA
+
{{ $statsAbsensi['alpha'] ?? 0 }}
+
Tanpa keterangan
+
+
+ + {{-- ── TOOLBAR FILTER ── --}} +
+
+ +
+ +
+ + +
+
+ +
+ + @if(request()->hasAny(['tanggal'])) + + Reset + + @endif +
+
+
+ + {{-- ── TABLE PANEL ── --}} +
+
+
+ + Data Laporan Kehadiran +
+
+ {{ $data->total() }} data ditemukan +
+
+ +
+ + + + + + + + + + + @forelse($data as $item) + + + + + + + @empty + + + + @endforelse + +
TeknisiTanggalStatus KehadiranKeterangan
+
+
{{ strtoupper(substr($item->nama_teknisi ?? '?', 0, 2)) }}
+
{{ $item->nama_teknisi ?? '-' }}
+
+
{{ \Carbon\Carbon::parse($item->tanggal)->format('d M Y') }}
+ @php + $st = strtolower($item->status); + $bc = 'pug-badge-amber'; $ic = 'fa-clock'; + if($st == 'hadir') { $bc = 'pug-badge-green'; $ic = 'fa-check-circle'; } + if(in_array($st, ['alpha','sakit'])) { $bc = 'pug-badge-rose'; $ic = 'fa-times-circle'; } + if($st == 'izin') { $bc = 'pug-badge-violet'; $ic = 'fa-info-circle'; } + @endphp + + {{ strtoupper($item->status) }} + + + {{ $item->keterangan ?? '-' }} + @if(!empty($item->latitude) && !empty($item->longitude)) + + Cek Lokasi + + @endif +
+
+
+
Tidak ada data kehadiran
+
Ubah filter pencarian untuk melihat data lain.
+
+
+
+ + @if($data->hasPages()) +
+ {{ $data->links() }} +
+ @endif +
+ +
+
+
diff --git a/samooapk/laravel/resources/views/Admin/Laporan/data_teknisi.blade.php b/samooapk/laravel/resources/views/Admin/Laporan/data_teknisi.blade.php new file mode 100644 index 0000000..6d1115c --- /dev/null +++ b/samooapk/laravel/resources/views/Admin/Laporan/data_teknisi.blade.php @@ -0,0 +1,194 @@ + +@push('styles') + + +@endpush + +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+

Laporan Data Teknisi

+

Daftar lengkap seluruh teknisi beserta status aktif • {{ date('F Y') }}

+
+
+ + Cetak Laporan + +
+ + {{-- ── TAB NAVIGATION ── --}} + + + {{-- ── STATS ── --}} +
+
+
+
TOTAL TEKNISI
+
{{ $statsTeknisi['total'] ?? 0 }}
+
Seluruh teknisi terdaftar
+
+
+
+
TEKNISI AKTIF
+
{{ $statsTeknisi['aktif'] ?? 0 }}
+
Status akun aktif
+
+
+
+
NONAKTIF
+
{{ $statsTeknisi['nonaktif'] ?? 0 }}
+
Status akun tidak aktif
+
+
+ + {{-- ── TOOLBAR FILTER ── --}} +
+
+ +
+ +
+ + +
+
+ +
+ + +
+ +
+ + @if(request()->hasAny(['search', 'status'])) + + Reset + + @endif +
+
+
+ + {{-- ── TABLE PANEL ── --}} +
+
+
+ + Data Lengkap Teknisi +
+
+ {{ $data->total() }} data ditemukan +
+
+ +
+ + + + + + + + + + + @forelse($data as $item) + + + + + + + @empty + + + + @endforelse + +
TeknisiNo TelpTgl MasukStatus
+
+
{{ strtoupper(substr($item->nama ?? '?', 0, 2)) }}
+
+
{{ $item->nama ?? '-' }}
+
{{ $item->email ?? '-' }}
+
+
+
+
+ {{ $item->no_telephone ?? '-' }} +
+
+
{{ $item->tanggal_masuk ? \Carbon\Carbon::parse($item->tanggal_masuk)->format('d M Y') : '-' }}
+
+ + {{ strtoupper(str_replace('_', ' ', $item->status ?? '-')) }} + +
+
+
+
Tidak ada data teknisi
+
Ubah filter pencarian untuk melihat data lain.
+
+
+
+ + @if($data->hasPages()) +
+ {{ $data->links() }} +
+ @endif +
+ +
+
+
diff --git a/samooapk/laravel/resources/views/Admin/Laporan/kasbon.blade.php b/samooapk/laravel/resources/views/Admin/Laporan/kasbon.blade.php new file mode 100644 index 0000000..963acbd --- /dev/null +++ b/samooapk/laravel/resources/views/Admin/Laporan/kasbon.blade.php @@ -0,0 +1,206 @@ + +@push('styles') + + +@endpush + +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+

Laporan Kasbon

+

Rincian pinjaman dan status pelunasan teknisi • {{ date('F Y') }}

+
+
+ + Cetak Laporan + +
+ + {{-- ── TAB NAVIGATION ── --}} + + + {{-- ── STATS ── --}} +
+
+
+
TOTAL KASBON
+
{{ $statsKasbon['total'] ?? 0 }}
+
Rp {{ number_format($statsKasbon['total_nominal'] ?? 0, 0, ',', '.') }}
+
+
+
+
LUNAS
+
{{ $statsKasbon['lunas'] ?? 0 }}
+
Kasbon yang sudah dibayar
+
+
+
+
BELUM LUNAS
+
{{ $statsKasbon['belum_lunas'] ?? 0 }}
+
Menunggu pelunasan
+
+
+
+
NOMINAL BELUM LUNAS
+
Rp {{ number_format($statsKasbon['total_belum_lunas'] ?? 0, 0, ',', '.') }}
+
Total tagihan tertunggak
+
+
+ + {{-- ── TOOLBAR FILTER ── --}} +
+
+ +
+ +
+ + +
+
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+ + @if(request()->hasAny(['search', 'status', 'id_teknisi'])) + + Reset + + @endif +
+
+
+ + {{-- ── TABLE PANEL ── --}} +
+
+
+ + Data Laporan Kasbon +
+
+ {{ $data->total() }} data ditemukan +
+
+ +
+ + + + + + + + + + + + @forelse($data as $item) + + + + + + + + @empty + + + + @endforelse + +
TeknisiTanggal PinjamJumlah KasbonStatusKeperluan
+
+
{{ strtoupper(substr($item->nama_teknisi ?? '?', 0, 2)) }}
+
{{ $item->nama_teknisi ?? '-' }}
+
+
{{ \Carbon\Carbon::parse($item->tanggal_kasbon)->format('d M Y') }}
Rp {{ number_format($item->jumlah_kasbon, 0, ',', '.') }}
+ + {{ strtoupper($item->status) }} + + {{ $item->keperluan ?? '-' }}
+
+
+
Tidak ada data kasbon
+
Ubah filter pencarian untuk melihat data lain.
+
+
+
+ + @if($data->hasPages()) +
+ {{ $data->links() }} +
+ @endif +
+ +
+
+
diff --git a/samooapk/laravel/resources/views/Admin/Laporan/pekerjaan.blade.php b/samooapk/laravel/resources/views/Admin/Laporan/pekerjaan.blade.php new file mode 100644 index 0000000..c69a38e --- /dev/null +++ b/samooapk/laravel/resources/views/Admin/Laporan/pekerjaan.blade.php @@ -0,0 +1,220 @@ + +@push('styles') + + +@endpush + +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+

Laporan Pekerjaan

+

Daftar penugasan dan status pengerjaan teknisi • {{ date('F Y') }}

+
+
+ + Cetak Laporan + +
+ + {{-- ── TAB NAVIGATION ── --}} + + + {{-- ── STATS ── --}} +
+
+
+
TOTAL PENUGASAN
+
{{ $statsPekerjaan['total'] ?? 0 }}
+
Seluruh penugasan
+
+
+
+
SELESAI
+
{{ $statsPekerjaan['selesai'] ?? 0 }}
+
Pekerjaan tuntas
+
+
+
+
DALAM PROSES
+
{{ $statsPekerjaan['proses'] ?? 0 }}
+
Sedang dikerjakan
+
+
+
+
PENDING
+
{{ $statsPekerjaan['pending'] ?? 0 }}
+
Menunggu dikerjakan
+
+
+ + {{-- ── TOOLBAR FILTER ── --}} +
+
+ +
+ +
+ + +
+
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+ + @if(request()->hasAny(['tanggal', 'status', 'id_teknisi'])) + + Reset + + @endif +
+
+
+ + {{-- ── TABLE PANEL ── --}} +
+
+
+ + Data Laporan Pekerjaan +
+
+ {{ $data->total() }} data ditemukan +
+
+ +
+ + + + + + + + + + + + @forelse($data as $item) + + + + + + + + @empty + + + + @endforelse + +
PekerjaanTeknisiLokasiStatusTgl Selesai
+
+
{{ $item->jenis_pekerjaan ?? '-' }}
+
#{{ $item->id_penugasan }}
+
+
+
+
{{ strtoupper(substr($item->nama_teknisi ?? '?', 0, 2)) }}
+
{{ $item->nama_teknisi ?? '-' }}
+
+
+
{{ $item->catatan_admin ?? '-' }}
+
+ @php + $sp = strtolower($item->status_pekerjaan ?? 'pending'); + $bcp = 'pug-badge-muted'; $icp = 'fa-clock'; + if($sp == 'selesai') { $bcp = 'pug-badge-green'; $icp = 'fa-check-circle'; } + if($sp == 'proses' || $sp == 'dalam_proses') { $bcp = 'pug-badge-violet'; $icp = 'fa-spinner'; } + @endphp + + {{ strtoupper(str_replace('_', ' ', $item->status_pekerjaan ?? '-')) }} + +
{{ $item->tanggal_diselesaikan ? \Carbon\Carbon::parse($item->tanggal_diselesaikan)->format('d M Y') : '-' }}
+
+
+
Tidak ada data pekerjaan
+
Ubah filter pencarian untuk melihat data lain.
+
+
+
+ + @if($data->hasPages()) +
+ {{ $data->links() }} +
+ @endif +
+ +
+
+
diff --git a/samooapk/laravel/resources/views/Admin/Laporan/penggajian.blade.php b/samooapk/laravel/resources/views/Admin/Laporan/penggajian.blade.php new file mode 100644 index 0000000..18ab488 --- /dev/null +++ b/samooapk/laravel/resources/views/Admin/Laporan/penggajian.blade.php @@ -0,0 +1,204 @@ + +@push('styles') + + +@endpush + +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+

Laporan Penggajian

+

Rincian penggajian dan status pembayaran teknisi • {{ date('F Y') }}

+
+
+ + Cetak Laporan + +
+ + {{-- ── TAB NAVIGATION ── --}} + + + {{-- ── STATS ── --}} +
+
+
+
TOTAL PENGGAJIAN
+
{{ $statsPenggajian['total'] ?? 0 }}
+
Keseluruhan data penggajian
+
+
+
+
LUNAS
+
{{ $statsPenggajian['lunas'] ?? 0 }}
+
Penggajian telah dibayar
+
+
+
+
BELUM DIBAYAR
+
{{ $statsPenggajian['belum'] ?? 0 }}
+
Menunggu pembayaran
+
+
+ + {{-- ── TOOLBAR FILTER ── --}} +
+
+ +
+ +
+ + +
+
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+ + @if(request()->hasAny(['tanggal', 'status', 'id_teknisi'])) + + Reset + + @endif +
+
+
+ + {{-- ── TABLE PANEL ── --}} +
+
+
+ + Data Laporan Penggajian +
+
+ {{ $data->total() }} data ditemukan +
+
+ +
+ + + + + + + + + + + @forelse($data as $item) + + + + + + + @empty + + + + @endforelse + +
TeknisiPeriodeGaji BersihStatus Pembayaran
+
+
{{ strtoupper(substr($item->nama_teknisi ?? '?', 0, 2)) }}
+
{{ $item->nama_teknisi ?? '-' }}
+
+
+ @php + $bulan = [1=>'Januari',2=>'Februari',3=>'Maret',4=>'April',5=>'Mei',6=>'Juni',7=>'Juli',8=>'Agustus',9=>'September',10=>'Oktober',11=>'November',12=>'Desember']; + $namaBulan = $bulan[(int)$item->periode_bulan] ?? '-'; + @endphp +
{{ $namaBulan }} {{ $item->periode_tahun }}
+
Rp {{ number_format($item->gaji_bersih, 0, ',', '.') }}
+ + {{ strtoupper(str_replace('_', ' ', $item->status_pembayaran)) }} + +
+
+
+
Tidak ada data penggajian
+
Ubah filter pencarian untuk melihat data lain.
+
+
+
+ + @if($data->hasPages()) +
+ {{ $data->links() }} +
+ @endif +
+ +
+
+
diff --git a/samooapk/laravel/resources/views/Admin/Laporan/print.blade.php b/samooapk/laravel/resources/views/Admin/Laporan/print.blade.php new file mode 100644 index 0000000..ba9ff5a --- /dev/null +++ b/samooapk/laravel/resources/views/Admin/Laporan/print.blade.php @@ -0,0 +1,234 @@ + + + + + + Cetak Laporan - {{ strtoupper($title ?? 'Laporan') }} + + + + +
+ + +
+ +
+ Logo PDAM +

PERUSAHAAN DAERAH AIR MINUM

+

(PDAM)

+

Jl. Kendedes No.8, Gianyar, Kec. Gianyar, Kabupaten Gianyar, Bali 80511

+

Telp: +62 823-3152-7309 | Email: tekleperumda@gmail.com

+
+ +
+ LAPORAN {{ $title ?? 'DATA' }} +
+ +
+ @if(!empty($filters['tanggal_dari']) || !empty($filters['tanggal_sampai'])) + Periode: + {{ !empty($filters['tanggal_dari']) ? \Carbon\Carbon::parse($filters['tanggal_dari'])->format('d M Y') : 'Awal' }} + s/d + {{ !empty($filters['tanggal_sampai']) ? \Carbon\Carbon::parse($filters['tanggal_sampai'])->format('d M Y') : 'Sekarang' }}
+ @else + Periode: Semua Waktu
+ @endif + + @if(!empty($filters['status'])) + Status: {{ strtoupper(str_replace('_', ' ', $filters['status'])) }}
+ @endif + + @if(!empty($filters['search'])) + Pencarian: "{{ $filters['search'] }}"
+ @endif +
+ + + + + + @foreach($columns as $col) + + @endforeach + + + + @forelse($data as $index => $item) + + + + @if($type == 'kasbon') + + + + + + + @elseif($type == 'teknisi') + + + + + + + + + + @elseif($type == 'absensi') + + + + + + @elseif($type == 'pekerjaan') + + + + + + + + @elseif($type == 'penggajian') + + + + + + @elseif($type == 'data_teknisi') + + + + + + @endif + + @empty + + + + @endforelse + +
No{{ $col }}
{{ $index + 1 }}{{ $item->nama_teknisi ?? '-' }}{{ \Carbon\Carbon::parse($item->tanggal_kasbon)->format('d/m/Y') }}Rp {{ number_format($item->jumlah_kasbon, 0, ',', '.') }}{{ strtoupper($item->status) }}{{ $item->keperluan ?? '-' }}{{ $item->nama }}{{ strtoupper($item->status) }}{{ $item->hadir }}{{ $item->izin }}{{ $item->sakit ?? 0 }}{{ $item->alpha }}{{ $item->total_absensi }} + {{ $item->total_absensi > 0 ? round(($item->hadir / $item->total_absensi) * 100, 1) : 0 }}% + {{ $item->nama_teknisi ?? '-' }}{{ \Carbon\Carbon::parse($item->tanggal)->format('d/m/Y') }}{{ strtoupper($item->status) }}{{ $item->keterangan ?? '-' }}{{ $item->id_penugasan }}{{ $item->jenis_pekerjaan ?? '-' }}{{ $item->nama_teknisi ?? '-' }}{{ strtoupper(str_replace('_', ' ', $item->status_pekerjaan)) }}{{ $item->tanggal_mulai ? \Carbon\Carbon::parse($item->tanggal_mulai)->format('d/m/Y') : '-' }}{{ $item->tanggal_diselesaikan ? \Carbon\Carbon::parse($item->tanggal_diselesaikan)->format('d/m/Y') : '-' }}{{ $item->nama_teknisi ?? '-' }} + @php + $bulan = [1=>'Januari',2=>'Februari',3=>'Maret',4=>'April',5=>'Mei',6=>'Juni',7=>'Juli',8=>'Agustus',9=>'September',10=>'Oktober',11=>'November',12=>'Desember']; + $namaBulan = $bulan[(int)$item->periode_bulan] ?? '-'; + @endphp + {{ $namaBulan }} {{ $item->periode_tahun }} + Rp {{ number_format($item->gaji_bersih, 0, ',', '.') }}{{ strtoupper(str_replace('_', ' ', $item->status_pembayaran)) }}{{ $item->nama ?? '-' }}{{ $item->email ?? '-' }}{{ $item->no_telephone ?? '-' }}{{ $item->tanggal_masuk ? \Carbon\Carbon::parse($item->tanggal_masuk)->format('d/m/Y') : '-' }}{{ strtoupper(str_replace('_', ' ', $item->status ?? '-')) }}
Tidak ada data yang sesuai dengan filter.
+ +
+
+ +
+
+

Gianyar, {{ \Carbon\Carbon::now()->format('d F Y') }}

+

Mandor Lapangan

+
+

(Kusaini)

+
+
+ + + diff --git a/samooapk/laravel/resources/views/Admin/Laporan/teknisi.blade.php b/samooapk/laravel/resources/views/Admin/Laporan/teknisi.blade.php new file mode 100644 index 0000000..3f57cea --- /dev/null +++ b/samooapk/laravel/resources/views/Admin/Laporan/teknisi.blade.php @@ -0,0 +1,173 @@ + +@push('styles') + + +@endpush + +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+

Performa Teknisi

+

Analisis produktivitas dan persentase kehadiran teknisi • {{ date('F Y') }}

+
+
+ + Cetak Laporan + +
+ + {{-- ── TAB NAVIGATION ── --}} + + + {{-- ── TOOLBAR FILTER ── --}} +
+
+
+ +
+ + +
+
+ +
+ + @if(request()->has('search') && request('search') != '') + + Reset + + @endif +
+
+
+ + {{-- ── TABLE PANEL ── --}} +
+
+
+ + Data Performa Teknisi +
+
+ {{ count($data) }} data ditemukan +
+
+ +
+ + + + + + + + + + + + + + @forelse($data as $item) + + + + + + + + + + @empty + + + + @endforelse + +
TeknisiStatus AkunHadirIzinSakitAlphaTingkat Kehadiran
+
+
{{ strtoupper(substr($item->nama ?? '?', 0, 2)) }}
+
{{ $item->nama ?? '-' }}
+
+
+ + {{ strtoupper($item->status) }} + + {{ $item->hadir }}{{ $item->izin }}{{ $item->sakit ?? 0 }}{{ $item->alpha }} + @php + $persentase = $item->total_absensi > 0 ? round(($item->hadir / $item->total_absensi) * 100, 1) : 0; + $progClass = 'bg-success'; + if ($persentase < 80 && $persentase >= 50) $progClass = 'bg-warning'; + if ($persentase < 50) $progClass = 'bg-danger'; + @endphp +
+ {{ $persentase }}% + {{ $item->total_absensi }} Hari +
+
+
+
+
+
+
+
Tidak ada data teknisi
+
Coba ubah kata kunci pencarian.
+
+
+
+
+ +
+
+
diff --git a/samooapk/laravel/resources/views/Admin/dashboard.blade.php b/samooapk/laravel/resources/views/Admin/dashboard.blade.php new file mode 100644 index 0000000..79f5d14 --- /dev/null +++ b/samooapk/laravel/resources/views/Admin/dashboard.blade.php @@ -0,0 +1,143 @@ + +
+ +
+ + +
+

Dashboard Overview

+
+ + +
+ +
+
+
+

Total Teknisi

+

+ {{ isset($totalTeknisi) ? $totalTeknisi : 0 }} +

+
+
+ +
+
+
+ + +
+
+
+

Teknisi Aktif

+

+ {{ isset($teknisiAktif) ? $teknisiAktif : 0 }} +

+
+
+ +
+
+
+ + +
+
+
+

Total Pekerjaan

+

0

+
+
+ +
+
+
+ + +
+
+
+

Total Laporan

+

0

+
+
+ +
+
+
+
+ + +
+ + + + +
+
+
+
User Information
+
+
+
+ Name:
+ {{ Auth::user()->name }} +
+
+ Email:
+ {{ Auth::user()->email }} +
+
+ Registered:
+ {{ Auth::user()->created_at->format('d M Y') }} +
+
+
+ @csrf + +
+
+
+
+
+
+
+
--> \ No newline at end of file diff --git a/samooapk/laravel/resources/views/auth/confirm-password.blade.php b/samooapk/laravel/resources/views/auth/confirm-password.blade.php new file mode 100644 index 0000000..3d38186 --- /dev/null +++ b/samooapk/laravel/resources/views/auth/confirm-password.blade.php @@ -0,0 +1,27 @@ + +
+ {{ __('This is a secure area of the application. Please confirm your password before continuing.') }} +
+ +
+ @csrf + + +
+ + + + + +
+ +
+ + {{ __('Confirm') }} + +
+
+
diff --git a/samooapk/laravel/resources/views/auth/forgot-password.blade.php b/samooapk/laravel/resources/views/auth/forgot-password.blade.php new file mode 100644 index 0000000..50b17a8 --- /dev/null +++ b/samooapk/laravel/resources/views/auth/forgot-password.blade.php @@ -0,0 +1,332 @@ + + + + + + Lupa Password - SIPDAM + + + + + + + + + +
+ +

Reset Password

+

Kami akan mengirimkan link reset password ke email Anda

+
+
+ + + +
+

Cek Email Anda

+

Setelah submit, cek inbox atau folder spam email Anda untuk link reset password.

+
+
+ + +
+
+ +
+ +
+ + + +
+

Lupa Password?

+

Masukkan email Anda dan kami akan mengirimkan link untuk reset password.

+
+ + @if (session('status')) +
{{ session('status') }}
+ @endif + +
+ @csrf + +
+ + + @error('email') +
{{ $message }}
+ @enderror +
+ + +
+ + + +
+
+ + + \ No newline at end of file diff --git a/samooapk/laravel/resources/views/auth/login.blade.php b/samooapk/laravel/resources/views/auth/login.blade.php new file mode 100644 index 0000000..d4fda73 --- /dev/null +++ b/samooapk/laravel/resources/views/auth/login.blade.php @@ -0,0 +1,453 @@ + + + + + + Login - SIPDAM + + + + + + + + + +
+ +

Selamat Datang di SIPDAM

+

Sistem Informasi Penggajian PDAM untuk pengelolaan tenaga kerja lepas PERUMDA Tirta Sanjiwani

+
+
+
+ + + +
+
+
Manajemen Tenaga Kerja
+
Data terpusat & terstruktur
+
+
+
+
+ + + +
+
+
Penggajian Otomatis
+
Akurat & tepat waktu
+
+
+
+
+ + + +
+
+
Absensi & Kontrak
+
Monitoring real-time
+
+
+
+
+ + +
+ +
+ + + + + \ No newline at end of file diff --git a/samooapk/laravel/resources/views/auth/register.blade.php b/samooapk/laravel/resources/views/auth/register.blade.php new file mode 100644 index 0000000..52d40b8 --- /dev/null +++ b/samooapk/laravel/resources/views/auth/register.blade.php @@ -0,0 +1,358 @@ + + + + + + Daftar - SIPDAM + + + + + + + + + +
+ +

Bergabung dengan SIPDAM

+

Buat akun untuk mengakses sistem penggajian tenaga kerja lepas PERUMDA Tirta Sanjiwani

+
+
+
+ + + +
+
+
Manajemen Tenaga Kerja
+
Data terpusat & terstruktur
+
+
+
+
+ + + +
+
+
Penggajian Otomatis
+
Akurat & tepat waktu
+
+
+
+
+ + + +
+
+
Absensi & Kontrak
+
Monitoring real-time
+
+
+
+
+ + +
+
+ +
+ +

Buat Akun Baru

+

Isi data di bawah untuk mendaftar ke SIPDAM

+
+ +
+ @csrf + +
+ + + @error('name') +
{{ $message }}
+ @enderror +
+ +
+ + + @error('email') +
{{ $message }}
+ @enderror +
+ +
+ + + @error('password') +
{{ $message }}
+ @enderror +
+ +
+ + + @error('password_confirmation') +
{{ $message }}
+ @enderror +
+ + +
+ + + +
+
+ + + \ No newline at end of file diff --git a/samooapk/laravel/resources/views/auth/reset-password.blade.php b/samooapk/laravel/resources/views/auth/reset-password.blade.php new file mode 100644 index 0000000..44c0d00 --- /dev/null +++ b/samooapk/laravel/resources/views/auth/reset-password.blade.php @@ -0,0 +1,379 @@ + + + + + + Reset Password - SIPDAM + + + + + + + + + +
+ +

Atur Ulang Password

+

Silakan buat password baru yang kuat untuk melindungi akun Anda

+
+
+ + + +
+

Gunakan Password Kuat

+

Disarankan menggunakan kombinasi huruf, angka, dan karakter spesial agar akun Anda lebih aman.

+
+
+ + +
+
+ +
+ +
+ + + +
+

Buat Password Baru

+

Masukkan email dan password baru Anda untuk melanjutkan.

+
+ +
+ @csrf + + + + +
+ + + @error('email') +
{{ $message }}
+ @enderror +
+ +
+ +
+ + + + + + + +
+ @error('password') +
{{ $message }}
+ @enderror +
+ +
+ +
+ + + + + + + +
+ @error('password_confirmation') +
{{ $message }}
+ @enderror +
+ + +
+ +
+
+ + + + + diff --git a/samooapk/laravel/resources/views/auth/verify-email.blade.php b/samooapk/laravel/resources/views/auth/verify-email.blade.php new file mode 100644 index 0000000..eaf811d --- /dev/null +++ b/samooapk/laravel/resources/views/auth/verify-email.blade.php @@ -0,0 +1,31 @@ + +
+ {{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }} +
+ + @if (session('status') == 'verification-link-sent') +
+ {{ __('A new verification link has been sent to the email address you provided during registration.') }} +
+ @endif + +
+
+ @csrf + +
+ + {{ __('Resend Verification Email') }} + +
+
+ +
+ @csrf + + +
+
+
diff --git a/samooapk/laravel/resources/views/components/application-logo.blade.php b/samooapk/laravel/resources/views/components/application-logo.blade.php new file mode 100644 index 0000000..46579cf --- /dev/null +++ b/samooapk/laravel/resources/views/components/application-logo.blade.php @@ -0,0 +1,3 @@ + + + diff --git a/samooapk/laravel/resources/views/components/auth-session-status.blade.php b/samooapk/laravel/resources/views/components/auth-session-status.blade.php new file mode 100644 index 0000000..c4bd6e2 --- /dev/null +++ b/samooapk/laravel/resources/views/components/auth-session-status.blade.php @@ -0,0 +1,7 @@ +@props(['status']) + +@if ($status) +
merge(['class' => 'font-medium text-sm text-green-600']) }}> + {{ $status }} +
+@endif diff --git a/samooapk/laravel/resources/views/components/danger-button.blade.php b/samooapk/laravel/resources/views/components/danger-button.blade.php new file mode 100644 index 0000000..d17d288 --- /dev/null +++ b/samooapk/laravel/resources/views/components/danger-button.blade.php @@ -0,0 +1,3 @@ + diff --git a/samooapk/laravel/resources/views/components/dropdown-link.blade.php b/samooapk/laravel/resources/views/components/dropdown-link.blade.php new file mode 100644 index 0000000..e0f8ce1 --- /dev/null +++ b/samooapk/laravel/resources/views/components/dropdown-link.blade.php @@ -0,0 +1 @@ +merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out']) }}>{{ $slot }} diff --git a/samooapk/laravel/resources/views/components/dropdown.blade.php b/samooapk/laravel/resources/views/components/dropdown.blade.php new file mode 100644 index 0000000..db38742 --- /dev/null +++ b/samooapk/laravel/resources/views/components/dropdown.blade.php @@ -0,0 +1,43 @@ +@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white']) + +@php +switch ($align) { + case 'left': + $alignmentClasses = 'ltr:origin-top-left rtl:origin-top-right start-0'; + break; + case 'top': + $alignmentClasses = 'origin-top'; + break; + case 'right': + default: + $alignmentClasses = 'ltr:origin-top-right rtl:origin-top-left end-0'; + break; +} + +switch ($width) { + case '48': + $width = 'w-48'; + break; +} +@endphp + +
+
+ {{ $trigger }} +
+ + +
diff --git a/samooapk/laravel/resources/views/components/input-error.blade.php b/samooapk/laravel/resources/views/components/input-error.blade.php new file mode 100644 index 0000000..9e6da21 --- /dev/null +++ b/samooapk/laravel/resources/views/components/input-error.blade.php @@ -0,0 +1,9 @@ +@props(['messages']) + +@if ($messages) +
    merge(['class' => 'text-sm text-red-600 space-y-1']) }}> + @foreach ((array) $messages as $message) +
  • {{ $message }}
  • + @endforeach +
+@endif diff --git a/samooapk/laravel/resources/views/components/input-label.blade.php b/samooapk/laravel/resources/views/components/input-label.blade.php new file mode 100644 index 0000000..1cc65e2 --- /dev/null +++ b/samooapk/laravel/resources/views/components/input-label.blade.php @@ -0,0 +1,5 @@ +@props(['value']) + + diff --git a/samooapk/laravel/resources/views/components/modal.blade.php b/samooapk/laravel/resources/views/components/modal.blade.php new file mode 100644 index 0000000..70704c1 --- /dev/null +++ b/samooapk/laravel/resources/views/components/modal.blade.php @@ -0,0 +1,78 @@ +@props([ + 'name', + 'show' => false, + 'maxWidth' => '2xl' +]) + +@php +$maxWidth = [ + 'sm' => 'sm:max-w-sm', + 'md' => 'sm:max-w-md', + 'lg' => 'sm:max-w-lg', + 'xl' => 'sm:max-w-xl', + '2xl' => 'sm:max-w-2xl', +][$maxWidth]; +@endphp + +
+
+
+
+ +
+ {{ $slot }} +
+
diff --git a/samooapk/laravel/resources/views/components/nav-link.blade.php b/samooapk/laravel/resources/views/components/nav-link.blade.php new file mode 100644 index 0000000..5c101a2 --- /dev/null +++ b/samooapk/laravel/resources/views/components/nav-link.blade.php @@ -0,0 +1,11 @@ +@props(['active']) + +@php +$classes = ($active ?? false) + ? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out' + : 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out'; +@endphp + +merge(['class' => $classes]) }}> + {{ $slot }} + diff --git a/samooapk/laravel/resources/views/components/primary-button.blade.php b/samooapk/laravel/resources/views/components/primary-button.blade.php new file mode 100644 index 0000000..d71f0b6 --- /dev/null +++ b/samooapk/laravel/resources/views/components/primary-button.blade.php @@ -0,0 +1,3 @@ + diff --git a/samooapk/laravel/resources/views/components/responsive-nav-link.blade.php b/samooapk/laravel/resources/views/components/responsive-nav-link.blade.php new file mode 100644 index 0000000..43b91e7 --- /dev/null +++ b/samooapk/laravel/resources/views/components/responsive-nav-link.blade.php @@ -0,0 +1,11 @@ +@props(['active']) + +@php +$classes = ($active ?? false) + ? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 text-start text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition duration-150 ease-in-out' + : 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out'; +@endphp + +merge(['class' => $classes]) }}> + {{ $slot }} + diff --git a/samooapk/laravel/resources/views/components/secondary-button.blade.php b/samooapk/laravel/resources/views/components/secondary-button.blade.php new file mode 100644 index 0000000..b32b69f --- /dev/null +++ b/samooapk/laravel/resources/views/components/secondary-button.blade.php @@ -0,0 +1,3 @@ + diff --git a/samooapk/laravel/resources/views/components/text-input.blade.php b/samooapk/laravel/resources/views/components/text-input.blade.php new file mode 100644 index 0000000..1df7f0d --- /dev/null +++ b/samooapk/laravel/resources/views/components/text-input.blade.php @@ -0,0 +1,3 @@ +@props(['disabled' => false]) + +merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) !!}> diff --git a/samooapk/laravel/resources/views/dashboard.blade.php b/samooapk/laravel/resources/views/dashboard.blade.php new file mode 100644 index 0000000..2a2cdfd --- /dev/null +++ b/samooapk/laravel/resources/views/dashboard.blade.php @@ -0,0 +1,272 @@ +{{-- resources/views/dashboard.blade.php --}} +{{-- CSS sudah dipisah ke: public/css/dashboard.css --}} + + +@push('scripts') + + +@endpush + +
+ + + + {{-- ── STAT CARDS ── --}} +
+
+
+
+ + + +
+
Total Teknisi
+
{{ $totalTeknisi ?? 0 }}
+
Terdaftar dalam sistem
+
+ +
+
+
+ + + +
+
Teknisi Aktif
+
{{ $teknisiAktif ?? 0 }}
+
Sedang bertugas
+
+ +
+
+
+ + + + +
+
Total Pekerjaan
+
{{ $totalPekerjaan ?? 0 }}
+
Semua penugasan
+
+ +
+
+
+ + + +
+
Total Laporan
+
{{ $totalLaporan ?? 0 }}
+
Laporan kegiatan
+
+
+ + {{-- ── BAR CHART ── --}} +
+
+
+
Penugasan per Jenis Pekerjaan — Bulan Ini
+
+
+ @foreach($chartLabels as $index => $label) + {{ $label }} + @endforeach +
+
+ +
+
+ + {{-- ── BAR CHART TEKNISI ── --}} +
+
+
+
Beban Kerja Teknisi — Seluruh Data
+
+
+ +
+
+ + {{-- ── TARIF PEKERJAAN (FULL WIDTH) ── --}} +
+
+
+
Tarif Pekerjaan — Referensi Cepat
+
+ + + + + + + + + + + @foreach($tarifPekerjaans ?? [] as $tarif) + + + + + + + @endforeach + +
PekerjaanKodeTarifSatuan
+ @php + $badgeClass = match($tarif->jenis_pekerjaan) { + 'sr' => 'bj-sr', + 'pengembangan_jaringan_pipa', + 'penyempurnaan_jaringan_pipa' => 'bj-pjp', + 'perbaikan_jaringan_pipa' => 'bj-perbaikan', + 'gali_urug' => 'bj-gali', + 'pemasangan_gate_valve', + 'pengangkatan' => 'bj-gv', + 'pengecatan_pipa_besi' => 'bj-cat', + default => 'bj-sr', + }; + $priceColor = match($tarif->jenis_pekerjaan) { + 'sr' => '#0f6e56', + 'pengembangan_jaringan_pipa', 'penyempurnaan_jaringan_pipa'=> '#185fa5', + 'perbaikan_jaringan_pipa' => '#a32d2d', + 'gali_urug' => '#633806', + 'pengecatan_pipa_besi' => '#72243e', + default => '#3c3489', + }; + @endphp + {{ $tarif->nama_item }} + {{ $tarif->kode_item }} + Rp {{ number_format($tarif->tarif_per_unit ?? $tarif->tarif_per_meter, 0, ',', '.') }} + + {{ $tarif->tarif_per_meter ? '/meter' : '/unit' }} +
+
+ + {{-- ── BOTTOM WIDGETS ── --}} +
+ + {{-- INFO PERUMDA --}} +
+ +
Tirta Sanjiwani
+
Kabupaten Gianyar, Bali
+
+
+ + + + (0361) 943233 +
+
+ + + + tekleperumda@gmail.com +
+
+ + {{-- DONUT CHART --}} +
+
+
+
Distribusi Pekerjaan
+
+
+
+ +
+
+
+ @foreach($chartLabels as $index => $label) + {{ $label }} + @endforeach +
+
+ +
+ +
+ +
\ No newline at end of file diff --git a/samooapk/laravel/resources/views/layouts/app.blade.php b/samooapk/laravel/resources/views/layouts/app.blade.php new file mode 100644 index 0000000..d23ac89 --- /dev/null +++ b/samooapk/laravel/resources/views/layouts/app.blade.php @@ -0,0 +1,221 @@ + + + + + + + + {{ config('app.name', 'SIPDAM') }} + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + + + + @stack('styles') {{-- ✅ ini saja yang ditambah di app.blade.php --}} + @stack('scripts') + + {{-- ✅ Override Tailwind --}} + + + + +
+ + @include('layouts.navigation') + +
+ +
+
+ +
+
+ Selamat datang, {{ Auth::user()->name }} +
+
+
+
+
+ + +
+
+
+ +
+ {{ $slot }} +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/samooapk/laravel/resources/views/layouts/guest.blade.php b/samooapk/laravel/resources/views/layouts/guest.blade.php new file mode 100644 index 0000000..11feb47 --- /dev/null +++ b/samooapk/laravel/resources/views/layouts/guest.blade.php @@ -0,0 +1,30 @@ + + + + + + + + {{ config('app.name', 'Laravel') }} + + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + +
+
+ + + +
+ +
+ {{ $slot }} +
+
+ + diff --git a/samooapk/laravel/resources/views/layouts/navigation.blade.php b/samooapk/laravel/resources/views/layouts/navigation.blade.php new file mode 100644 index 0000000..ef5a0cc --- /dev/null +++ b/samooapk/laravel/resources/views/layouts/navigation.blade.php @@ -0,0 +1,150 @@ +{{-- resources/views/layouts/navigation.blade.php --}} + + \ No newline at end of file diff --git a/samooapk/laravel/resources/views/profile/edit.blade.php b/samooapk/laravel/resources/views/profile/edit.blade.php new file mode 100644 index 0000000..e0e1d38 --- /dev/null +++ b/samooapk/laravel/resources/views/profile/edit.blade.php @@ -0,0 +1,29 @@ + + +

+ {{ __('Profile') }} +

+
+ +
+
+
+
+ @include('profile.partials.update-profile-information-form') +
+
+ +
+
+ @include('profile.partials.update-password-form') +
+
+ +
+
+ @include('profile.partials.delete-user-form') +
+
+
+
+
diff --git a/samooapk/laravel/resources/views/profile/index.blade.php b/samooapk/laravel/resources/views/profile/index.blade.php new file mode 100644 index 0000000..d08a5ea --- /dev/null +++ b/samooapk/laravel/resources/views/profile/index.blade.php @@ -0,0 +1,47 @@ +@extends('layouts.app') + +@section('title', 'Profile') +@section('page-title', 'Profile') + +@section('content') +
+
+
+
+
+ +
+
+

Admin User

+

Administrator

+ + Edit Profile + +
+
+ +
+
+
Profile Information
+
+
+ Full Name: +

Admin User

+
+
+ Email: +

admin@example.com

+
+
+ Role: +

Administrator

+
+
+ Last Login: +

2 hours ago

+
+
+
+
+
+@endsection \ No newline at end of file diff --git a/samooapk/laravel/resources/views/profile/partials/delete-user-form.blade.php b/samooapk/laravel/resources/views/profile/partials/delete-user-form.blade.php new file mode 100644 index 0000000..edeeb4a --- /dev/null +++ b/samooapk/laravel/resources/views/profile/partials/delete-user-form.blade.php @@ -0,0 +1,55 @@ +
+
+

+ {{ __('Delete Account') }} +

+ +

+ {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }} +

+
+ + {{ __('Delete Account') }} + + +
+ @csrf + @method('delete') + +

+ {{ __('Are you sure you want to delete your account?') }} +

+ +

+ {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }} +

+ +
+ + + + + +
+ +
+ + {{ __('Cancel') }} + + + + {{ __('Delete Account') }} + +
+
+
+
diff --git a/samooapk/laravel/resources/views/profile/partials/update-password-form.blade.php b/samooapk/laravel/resources/views/profile/partials/update-password-form.blade.php new file mode 100644 index 0000000..eaca1ac --- /dev/null +++ b/samooapk/laravel/resources/views/profile/partials/update-password-form.blade.php @@ -0,0 +1,48 @@ +
+
+

+ {{ __('Update Password') }} +

+ +

+ {{ __('Ensure your account is using a long, random password to stay secure.') }} +

+
+ +
+ @csrf + @method('put') + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ {{ __('Save') }} + + @if (session('status') === 'password-updated') +

{{ __('Saved.') }}

+ @endif +
+
+
diff --git a/samooapk/laravel/resources/views/profile/partials/update-profile-information-form.blade.php b/samooapk/laravel/resources/views/profile/partials/update-profile-information-form.blade.php new file mode 100644 index 0000000..5ae3d35 --- /dev/null +++ b/samooapk/laravel/resources/views/profile/partials/update-profile-information-form.blade.php @@ -0,0 +1,64 @@ +
+
+

+ {{ __('Profile Information') }} +

+ +

+ {{ __("Update your account's profile information and email address.") }} +

+
+ +
+ @csrf +
+ +
+ @csrf + @method('patch') + +
+ + + +
+ +
+ + + + + @if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->hasVerifiedEmail()) +
+

+ {{ __('Your email address is unverified.') }} + + +

+ + @if (session('status') === 'verification-link-sent') +

+ {{ __('A new verification link has been sent to your email address.') }} +

+ @endif +
+ @endif +
+ +
+ {{ __('Save') }} + + @if (session('status') === 'profile-updated') +

{{ __('Saved.') }}

+ @endif +
+
+
diff --git a/samooapk/laravel/resources/views/welcome.blade.php b/samooapk/laravel/resources/views/welcome.blade.php new file mode 100644 index 0000000..35f6395 --- /dev/null +++ b/samooapk/laravel/resources/views/welcome.blade.php @@ -0,0 +1,614 @@ + + + + + + SIPDAM - Sistem Penggajian Tenaga Kerja Lepas | PERUMDA Tirta Sanjiwani + + + + + + + + + + +
+ + + +
+ + +
+
+ + + (0361) 943233 + + + + tekleperumda@gmail.com + +
+ +
+ + + + + + +
+
+
+ + Sistem Resmi PERUMDA Tirta Sanjiwani +
+

+ Kelola Tenaga Kerja Lepas dengan Mudah & Efisien +

+

+ Platform digital terintegrasi untuk pengelolaan data, kontrak, absensi, dan penggajian tenaga kerja lepas PERUMDA Air Minum Tirta Sanjiwani Kabupaten Gianyar. +

+
+ @if (Route::has('login')) + @auth + + + Buka Dashboard + + @else + + + Masuk Sekarang + + @if (Route::has('register')) + + + Daftar Akun + + @endif + @endauth + @endif +
+
+ +
+
+ +
+ Teknisi lapangan perbaikan pipa PDAM +
+ + +
+
+ + + +
+
+
100%
+
Data Terverifikasi
+
+
+ + +
+
+ + + +
+
+
Gaji Tepat Waktu
+
Otomatis & Akurat
+
+
+
+
+
+ + +
+

Fitur Unggulan Sistem

+
+
+
+ +
+

Data Pekerja

+

Kelola data lengkap tenaga kerja lepas secara terpusat dan terstruktur

+
+
+
+ +
+

Absensi Digital

+

Pencatatan kehadiran harian yang akurat dan mudah dipantau

+
+
+
+ +
+

Manajemen Pekerjaan

+

Pemantauan penugasan dan pelaporan progres kerja teknisi secara berkala

+
+
+
+ +
+

Penggajian

+

Perhitungan dan rekap gaji otomatis berdasarkan kehadiran

+
+
+
+ + +
+ PERUMDA Air Minum Tirta Sanjiwani · Kabupaten Gianyar, Bali  |  + SIPDAM © {{ date('Y') }} · Laravel v{{ Illuminate\Foundation\Application::VERSION }} +
+ + + \ No newline at end of file diff --git a/samooapk/laravel/routes/api.php b/samooapk/laravel/routes/api.php new file mode 100644 index 0000000..66f91ff --- /dev/null +++ b/samooapk/laravel/routes/api.php @@ -0,0 +1,89 @@ +get('/user', function (Request $request) { + return $request->user(); +}); + +// Routes untuk Teknisi (Mobile App) +Route::prefix('teknisi')->group(function () { + Route::post('/login', [AkunTeknisiController::class, 'login']); + + Route::middleware('auth:api')->group(function () { + Route::post('/logout', [AkunTeknisiController::class, 'logout']); + Route::get('/me', [AkunTeknisiController::class, 'me']); + Route::post('/refresh', [AkunTeknisiController::class, 'refresh']); + Route::post('/change-password', [AkunTeknisiController::class, 'changePassword']); + }); +}); + +Route::prefix('absensi')->group(function () { + Route::post('/absen-masuk', [AbsensiApiController::class, 'absenMasuk']); + Route::post('/absen-keluar', [AbsensiApiController::class, 'absenKeluar']); + Route::get('/check-status/{id_teknisi}', [AbsensiApiController::class, 'checkStatus']); + + // ✅ Route spesifik HARUS di atas /{id} + Route::get('/riwayat', [AbsensiApiController::class, 'riwayat']); + Route::get('/statistik', [AbsensiApiController::class, 'statistik']); + Route::get('/status/options', [AbsensiApiController::class, 'getStatusOptions']); + Route::get('/rekap', [AbsensiApiController::class, 'rekap']); + Route::get('/kalender', [AbsensiApiController::class, 'kalender']); + + // ⚠️ Route dinamis /{id} paling BAWAH + // Route::get('/', [AbsensiApiController::class, 'index']); + // Route::post('/', [AbsensiApiController::class, 'store']); + // Route::get('/{id}', [AbsensiApiController::class, 'show']); + // Route::put('/{id}', [AbsensiApiController::class, 'update']); + // Route::delete('/{id}', [AbsensiApiController::class, 'destroy']); +}); + +// ✅ Routes Penugasan (BARU) +Route::prefix('penugasan')->group(function () { + + // ⚠️ Route spesifik HARUS di atas route dengan parameter dinamis! + + // Master data + Route::get('/statistik', [PenugasanApiController::class, 'statistik']); + Route::get('/master/tarif-by-jenis', [PenugasanApiController::class, 'getTarifByJenis']); + Route::get('/master/teknisi-list', [PenugasanApiController::class, 'getTeknisiList']); + + // CRUD dasar + Route::get('/', [PenugasanApiController::class, 'index']); + Route::get('/{id}', [PenugasanApiController::class, 'show']); + + // Aksi spesifik per penugasan + Route::post('/{id}/lengkapi-detail', [PenugasanApiController::class, 'lengkapiDetail']); + Route::put('/{id}/update-detail', [PenugasanApiController::class, 'updateDetail']); // ✅ BARU untuk edit + Route::post('/{id}/add-item', [PenugasanApiController::class, 'addItem']); // ✅ BARU + Route::put('/{id}/update-status', [PenugasanApiController::class, 'updateStatus']); + Route::post('/{id}/upload-foto', [PenugasanApiController::class, 'uploadFoto']); +}); + +// ✅ Routes Gaji (Mobile) +Route::prefix('gaji')->group(function () { + Route::get('/riwayat', [GajiApiController::class, 'riwayat']); + Route::get('/{id}', [GajiApiController::class, 'show']); +}); + +// ✅ Routes Kasbon (Mobile) +Route::prefix('kasbon')->group(function () { + Route::get('/riwayat', [KasbonApiController::class, 'riwayat']); + Route::get('/statistik', [KasbonApiController::class, 'statistik']); +}); + +// ✅ Routes Dashboard (Mobile) +Route::get('/dashboard', [DashboardApiController::class, 'index']); \ No newline at end of file diff --git a/samooapk/laravel/routes/auth.php b/samooapk/laravel/routes/auth.php new file mode 100644 index 0000000..1040b51 --- /dev/null +++ b/samooapk/laravel/routes/auth.php @@ -0,0 +1,59 @@ +group(function () { + Route::get('register', [RegisteredUserController::class, 'create']) + ->name('register'); + + Route::post('register', [RegisteredUserController::class, 'store']); + + Route::get('login', [AuthenticatedSessionController::class, 'create']) + ->name('login'); + + Route::post('login', [AuthenticatedSessionController::class, 'store']); + + Route::get('forgot-password', [PasswordResetLinkController::class, 'create']) + ->name('password.request'); + + Route::post('forgot-password', [PasswordResetLinkController::class, 'store']) + ->name('password.email'); + + Route::get('reset-password/{token}', [NewPasswordController::class, 'create']) + ->name('password.reset'); + + Route::post('reset-password', [NewPasswordController::class, 'store']) + ->name('password.store'); +}); + +Route::middleware('auth')->group(function () { + Route::get('verify-email', EmailVerificationPromptController::class) + ->name('verification.notice'); + + Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) + ->middleware(['signed', 'throttle:6,1']) + ->name('verification.verify'); + + Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) + ->middleware('throttle:6,1') + ->name('verification.send'); + + Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) + ->name('password.confirm'); + + Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); + + Route::put('password', [PasswordController::class, 'update'])->name('password.update'); + + Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) + ->name('logout'); +}); diff --git a/samooapk/laravel/routes/channels.php b/samooapk/laravel/routes/channels.php new file mode 100644 index 0000000..5d451e1 --- /dev/null +++ b/samooapk/laravel/routes/channels.php @@ -0,0 +1,18 @@ +id === (int) $id; +}); diff --git a/samooapk/laravel/routes/console.php b/samooapk/laravel/routes/console.php new file mode 100644 index 0000000..e05f4c9 --- /dev/null +++ b/samooapk/laravel/routes/console.php @@ -0,0 +1,19 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); diff --git a/samooapk/laravel/routes/web.php b/samooapk/laravel/routes/web.php new file mode 100644 index 0000000..85ef980 --- /dev/null +++ b/samooapk/laravel/routes/web.php @@ -0,0 +1,130 @@ +group(function () { + Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard'); + + // ===== KELOLA TEKNISI ===== + Route::prefix('teknisi')->name('teknisi.')->group(function () { + Route::get('/', [TeknisiController::class, 'index'])->name('index'); + Route::post('/', [TeknisiController::class, 'store'])->name('store'); + Route::get('/{id}', [TeknisiController::class, 'show'])->name('show'); + Route::put('/{id}', [TeknisiController::class, 'update'])->name('update'); + Route::delete('/{id}', [TeknisiController::class, 'destroy'])->name('destroy'); + + // TAMBAHKAN INI - Redirect ke route absensi yang benar + Route::get('/absensi', function() { + return redirect()->route('absensi.index'); + })->name('absensi'); + }); + + // ===== AKUN TEKNISI ===== + Route::prefix('akun-teknisi')->name('akun-teknisi.')->group(function () { + Route::get('/', [AkunTeknisiController::class, 'index'])->name('index'); + Route::post('/', [AkunTeknisiController::class, 'store'])->name('store'); + Route::get('/{id}', [AkunTeknisiController::class, 'show'])->name('show'); + Route::get('/{id}/edit', [AkunTeknisiController::class, 'edit'])->name('edit'); + Route::put('/{id}', [AkunTeknisiController::class, 'update'])->name('update'); + Route::delete('/{id}', [AkunTeknisiController::class, 'destroy'])->name('destroy'); + Route::post('/{id}/update-status', [AkunTeknisiController::class, 'updateStatus'])->name('update-status'); + }); + + // ===== ABSENSI ===== + Route::prefix('absensi')->name('absensi.')->group(function () { + Route::get('/', [AbsensiController::class, 'index'])->name('index'); + Route::get('/{id}', [AbsensiController::class, 'show'])->name('show'); + Route::put('/{id}', [AbsensiController::class, 'update'])->name('update'); + }); + + // ===== KELOLA PEKERJAAN ===== + Route::prefix('pekerjaan')->name('pekerjaan.')->group(function () { + // Data Penugasan dengan CRUD lengkap + Route::prefix('penugasan')->name('penugasan.')->group(function () { + + // ✅ TAMBAHKAN INI - Route untuk ambil tarif + Route::get('/tarif-by-kategori', [PenugasanController::class, 'getTarifByKategori']) + ->name('getTarifByKategori'); + + + Route::get('/get-teknisi', [PenugasanController::class, 'getTeknisiByDate'])->name('get-teknisi'); + Route::get('/', [PenugasanController::class, 'index'])->name('index'); + Route::post('/', [PenugasanController::class, 'store'])->name('store'); + Route::get('/{id}', [PenugasanController::class, 'show'])->name('show'); + Route::get('/{id}/edit', [PenugasanController::class, 'edit'])->name('edit'); + Route::put('/{id}', [PenugasanController::class, 'update'])->name('update'); + Route::delete('/{id}', [PenugasanController::class, 'destroy'])->name('destroy'); + Route::post('/delete-multiple', [PenugasanController::class, 'destroyMultiple'])->name('delete-multiple'); + }); + + // Submenu: Monitoring Progres + Route::get('/monitoring', [PenugasanController::class, 'monitoring'])->name('monitoring'); +}); + + // ===== GAJI ===== + Route::prefix('gaji')->group(function () { + Route::get('/perhitungan', fn() => redirect()->route('penggajian.index'))->name('gaji.perhitungan'); + Route::get('/kasbon', fn() => redirect()->route('kasbon.index'))->name('gaji.kasbon'); + }); + + Route::post('/penggajian/hitung', [PenggajianController::class, 'hitungGaji'])->name('penggajian.hitung'); + Route::post('/penggajian/bayar-semua', [PenggajianController::class, 'prosesSemuaPembayaran'])->name('penggajian.bayar-semua'); + Route::post('/penggajian/{penggajian}/bayar', [PenggajianController::class, 'prosesPembayaran'])->name('penggajian.bayar'); + Route::get('/penggajian/export', [PenggajianController::class, 'export'])->name('penggajian.export'); + Route::get('/penggajian/{penggajian}/slip', [PenggajianController::class, 'slip'])->name('penggajian.slip'); + Route::get('/penggajian/{penggajian}/detail', [PenggajianController::class, 'detail'])->name('penggajian.detail'); + Route::post('/penggajian/{penggajian}/recalculate', [PenggajianController::class, 'recalculate']) + ->name('penggajian.recalculate'); + Route::post('/penggajian/{penggajian}/update-kasbon', [PenggajianController::class, 'updateKasbon']) + ->name('penggajian.update-kasbon'); + Route::post('/penggajian/{penggajian}/update-makan', [PenggajianController::class, 'updateMakan']) + ->name('penggajian.update-makan'); + Route::resource('penggajian', PenggajianController::class); + + Route::get('/kasbon/statistics', [KasbonController::class, 'statistics'])->name('kasbon.statistics'); +Route::post('/kasbon/{id}/lunas', [KasbonController::class, 'markAsLunas'])->name('kasbon.lunas'); +Route::resource('kasbon', KasbonController::class); + + // ===== LAPORAN ===== + Route::prefix('laporan')->name('laporan.')->group(function () { + Route::get('/', [LaporanController::class, 'index'])->name('index'); + Route::get('/statistics', [LaporanController::class, 'statistics'])->name('statistics'); + Route::get('/kasbon', [LaporanController::class, 'kasbon'])->name('kasbon'); + Route::get('/teknisi', [LaporanController::class, 'teknisi'])->name('teknisi'); + Route::get('/absensi', [LaporanController::class, 'absensi'])->name('absensi'); + Route::get('/pekerjaan', [LaporanController::class, 'pekerjaan'])->name('pekerjaan'); + Route::get('/penggajian', [LaporanController::class, 'penggajian'])->name('penggajian'); + Route::get('/data-teknisi', [LaporanController::class, 'dataTeknisi'])->name('data_teknisi'); + Route::get('/export', [LaporanController::class, 'export'])->name('export'); + Route::get('/kasbon/export', [LaporanController::class, 'exportKasbon'])->name('kasbon.export'); + Route::get('/teknisi/export', [LaporanController::class, 'exportTeknisi'])->name('teknisi.export'); + Route::get('/absensi/export', [LaporanController::class, 'exportAbsensi'])->name('absensi.export'); + Route::get('/pekerjaan/export', [LaporanController::class, 'exportPekerjaan'])->name('pekerjaan.export'); + Route::get('/penggajian/export', [LaporanController::class, 'exportPenggajian'])->name('penggajian.export'); + Route::get('/data-teknisi/export', [LaporanController::class, 'exportDataTeknisi'])->name('data_teknisi.export'); + }); + + // ===== PROFILE ===== + Route::prefix('profile')->name('profile.')->group(function () { + Route::get('/', [ProfileController::class, 'edit'])->name('edit'); + Route::patch('/', [ProfileController::class, 'update'])->name('update'); + Route::delete('/', [ProfileController::class, 'destroy'])->name('destroy'); + }); +}); + +require __DIR__.'/auth.php'; \ No newline at end of file diff --git a/samooapk/laravel/storage/app/.gitignore b/samooapk/laravel/storage/app/.gitignore new file mode 100644 index 0000000..8f4803c --- /dev/null +++ b/samooapk/laravel/storage/app/.gitignore @@ -0,0 +1,3 @@ +* +!public/ +!.gitignore diff --git a/samooapk/laravel/storage/app/public/.gitignore b/samooapk/laravel/storage/app/public/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/samooapk/laravel/storage/app/public/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/samooapk/laravel/storage/framework/.gitignore b/samooapk/laravel/storage/framework/.gitignore new file mode 100644 index 0000000..05c4471 --- /dev/null +++ b/samooapk/laravel/storage/framework/.gitignore @@ -0,0 +1,9 @@ +compiled.php +config.php +down +events.scanned.php +maintenance.php +routes.php +routes.scanned.php +schedule-* +services.json diff --git a/samooapk/laravel/storage/framework/cache/.gitignore b/samooapk/laravel/storage/framework/cache/.gitignore new file mode 100644 index 0000000..01e4a6c --- /dev/null +++ b/samooapk/laravel/storage/framework/cache/.gitignore @@ -0,0 +1,3 @@ +* +!data/ +!.gitignore diff --git a/samooapk/laravel/storage/framework/cache/data/.gitignore b/samooapk/laravel/storage/framework/cache/data/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/samooapk/laravel/storage/framework/cache/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/samooapk/laravel/storage/framework/sessions/.gitignore b/samooapk/laravel/storage/framework/sessions/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/samooapk/laravel/storage/framework/sessions/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/samooapk/laravel/storage/framework/testing/.gitignore b/samooapk/laravel/storage/framework/testing/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/samooapk/laravel/storage/framework/testing/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/samooapk/laravel/storage/framework/views/.gitignore b/samooapk/laravel/storage/framework/views/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/samooapk/laravel/storage/framework/views/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/samooapk/laravel/storage/logs/.gitignore b/samooapk/laravel/storage/logs/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/samooapk/laravel/storage/logs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/samooapk/laravel/tailwind.config.js b/samooapk/laravel/tailwind.config.js new file mode 100644 index 0000000..c29eb1a --- /dev/null +++ b/samooapk/laravel/tailwind.config.js @@ -0,0 +1,21 @@ +import defaultTheme from 'tailwindcss/defaultTheme'; +import forms from '@tailwindcss/forms'; + +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', + './storage/framework/views/*.php', + './resources/views/**/*.blade.php', + ], + + theme: { + extend: { + fontFamily: { + sans: ['Figtree', ...defaultTheme.fontFamily.sans], + }, + }, + }, + + plugins: [forms], +}; diff --git a/samooapk/laravel/tests/CreatesApplication.php b/samooapk/laravel/tests/CreatesApplication.php new file mode 100644 index 0000000..cc68301 --- /dev/null +++ b/samooapk/laravel/tests/CreatesApplication.php @@ -0,0 +1,21 @@ +make(Kernel::class)->bootstrap(); + + return $app; + } +} diff --git a/samooapk/laravel/tests/Feature/Auth/AuthenticationTest.php b/samooapk/laravel/tests/Feature/Auth/AuthenticationTest.php new file mode 100644 index 0000000..0303b29 --- /dev/null +++ b/samooapk/laravel/tests/Feature/Auth/AuthenticationTest.php @@ -0,0 +1,55 @@ +get('/login'); + + $response->assertStatus(200); + } + + public function test_users_can_authenticate_using_the_login_screen(): void + { + $user = User::factory()->create(); + + $response = $this->post('/login', [ + 'email' => $user->email, + 'password' => 'password', + ]); + + $this->assertAuthenticated(); + $response->assertRedirect(RouteServiceProvider::HOME); + } + + public function test_users_can_not_authenticate_with_invalid_password(): void + { + $user = User::factory()->create(); + + $this->post('/login', [ + 'email' => $user->email, + 'password' => 'wrong-password', + ]); + + $this->assertGuest(); + } + + public function test_users_can_logout(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/logout'); + + $this->assertGuest(); + $response->assertRedirect('/'); + } +} diff --git a/samooapk/laravel/tests/Feature/Auth/EmailVerificationTest.php b/samooapk/laravel/tests/Feature/Auth/EmailVerificationTest.php new file mode 100644 index 0000000..ba19d9c --- /dev/null +++ b/samooapk/laravel/tests/Feature/Auth/EmailVerificationTest.php @@ -0,0 +1,65 @@ +create([ + 'email_verified_at' => null, + ]); + + $response = $this->actingAs($user)->get('/verify-email'); + + $response->assertStatus(200); + } + + public function test_email_can_be_verified(): void + { + $user = User::factory()->create([ + 'email_verified_at' => null, + ]); + + Event::fake(); + + $verificationUrl = URL::temporarySignedRoute( + 'verification.verify', + now()->addMinutes(60), + ['id' => $user->id, 'hash' => sha1($user->email)] + ); + + $response = $this->actingAs($user)->get($verificationUrl); + + Event::assertDispatched(Verified::class); + $this->assertTrue($user->fresh()->hasVerifiedEmail()); + $response->assertRedirect(RouteServiceProvider::HOME.'?verified=1'); + } + + public function test_email_is_not_verified_with_invalid_hash(): void + { + $user = User::factory()->create([ + 'email_verified_at' => null, + ]); + + $verificationUrl = URL::temporarySignedRoute( + 'verification.verify', + now()->addMinutes(60), + ['id' => $user->id, 'hash' => sha1('wrong-email')] + ); + + $this->actingAs($user)->get($verificationUrl); + + $this->assertFalse($user->fresh()->hasVerifiedEmail()); + } +} diff --git a/samooapk/laravel/tests/Feature/Auth/PasswordConfirmationTest.php b/samooapk/laravel/tests/Feature/Auth/PasswordConfirmationTest.php new file mode 100644 index 0000000..ff85721 --- /dev/null +++ b/samooapk/laravel/tests/Feature/Auth/PasswordConfirmationTest.php @@ -0,0 +1,44 @@ +create(); + + $response = $this->actingAs($user)->get('/confirm-password'); + + $response->assertStatus(200); + } + + public function test_password_can_be_confirmed(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/confirm-password', [ + 'password' => 'password', + ]); + + $response->assertRedirect(); + $response->assertSessionHasNoErrors(); + } + + public function test_password_is_not_confirmed_with_invalid_password(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/confirm-password', [ + 'password' => 'wrong-password', + ]); + + $response->assertSessionHasErrors(); + } +} diff --git a/samooapk/laravel/tests/Feature/Auth/PasswordResetTest.php b/samooapk/laravel/tests/Feature/Auth/PasswordResetTest.php new file mode 100644 index 0000000..aa50350 --- /dev/null +++ b/samooapk/laravel/tests/Feature/Auth/PasswordResetTest.php @@ -0,0 +1,73 @@ +get('/forgot-password'); + + $response->assertStatus(200); + } + + public function test_reset_password_link_can_be_requested(): void + { + Notification::fake(); + + $user = User::factory()->create(); + + $this->post('/forgot-password', ['email' => $user->email]); + + Notification::assertSentTo($user, ResetPassword::class); + } + + public function test_reset_password_screen_can_be_rendered(): void + { + Notification::fake(); + + $user = User::factory()->create(); + + $this->post('/forgot-password', ['email' => $user->email]); + + Notification::assertSentTo($user, ResetPassword::class, function ($notification) { + $response = $this->get('/reset-password/'.$notification->token); + + $response->assertStatus(200); + + return true; + }); + } + + public function test_password_can_be_reset_with_valid_token(): void + { + Notification::fake(); + + $user = User::factory()->create(); + + $this->post('/forgot-password', ['email' => $user->email]); + + Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { + $response = $this->post('/reset-password', [ + 'token' => $notification->token, + 'email' => $user->email, + 'password' => 'password', + 'password_confirmation' => 'password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect(route('login')); + + return true; + }); + } +} diff --git a/samooapk/laravel/tests/Feature/Auth/PasswordUpdateTest.php b/samooapk/laravel/tests/Feature/Auth/PasswordUpdateTest.php new file mode 100644 index 0000000..ca28c6c --- /dev/null +++ b/samooapk/laravel/tests/Feature/Auth/PasswordUpdateTest.php @@ -0,0 +1,51 @@ +create(); + + $response = $this + ->actingAs($user) + ->from('/profile') + ->put('/password', [ + 'current_password' => 'password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/profile'); + + $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); + } + + public function test_correct_password_must_be_provided_to_update_password(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->from('/profile') + ->put('/password', [ + 'current_password' => 'wrong-password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]); + + $response + ->assertSessionHasErrorsIn('updatePassword', 'current_password') + ->assertRedirect('/profile'); + } +} diff --git a/samooapk/laravel/tests/Feature/Auth/RegistrationTest.php b/samooapk/laravel/tests/Feature/Auth/RegistrationTest.php new file mode 100644 index 0000000..30829b1 --- /dev/null +++ b/samooapk/laravel/tests/Feature/Auth/RegistrationTest.php @@ -0,0 +1,32 @@ +get('/register'); + + $response->assertStatus(200); + } + + public function test_new_users_can_register(): void + { + $response = $this->post('/register', [ + 'name' => 'Test User', + 'email' => 'test@example.com', + 'password' => 'password', + 'password_confirmation' => 'password', + ]); + + $this->assertAuthenticated(); + $response->assertRedirect(RouteServiceProvider::HOME); + } +} diff --git a/samooapk/laravel/tests/Feature/ExampleTest.php b/samooapk/laravel/tests/Feature/ExampleTest.php new file mode 100644 index 0000000..8364a84 --- /dev/null +++ b/samooapk/laravel/tests/Feature/ExampleTest.php @@ -0,0 +1,19 @@ +get('/'); + + $response->assertStatus(200); + } +} diff --git a/samooapk/laravel/tests/Feature/ProfileTest.php b/samooapk/laravel/tests/Feature/ProfileTest.php new file mode 100644 index 0000000..252fdcc --- /dev/null +++ b/samooapk/laravel/tests/Feature/ProfileTest.php @@ -0,0 +1,99 @@ +create(); + + $response = $this + ->actingAs($user) + ->get('/profile'); + + $response->assertOk(); + } + + public function test_profile_information_can_be_updated(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->patch('/profile', [ + 'name' => 'Test User', + 'email' => 'test@example.com', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/profile'); + + $user->refresh(); + + $this->assertSame('Test User', $user->name); + $this->assertSame('test@example.com', $user->email); + $this->assertNull($user->email_verified_at); + } + + public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->patch('/profile', [ + 'name' => 'Test User', + 'email' => $user->email, + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/profile'); + + $this->assertNotNull($user->refresh()->email_verified_at); + } + + public function test_user_can_delete_their_account(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->delete('/profile', [ + 'password' => 'password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/'); + + $this->assertGuest(); + $this->assertNull($user->fresh()); + } + + public function test_correct_password_must_be_provided_to_delete_account(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->from('/profile') + ->delete('/profile', [ + 'password' => 'wrong-password', + ]); + + $response + ->assertSessionHasErrorsIn('userDeletion', 'password') + ->assertRedirect('/profile'); + + $this->assertNotNull($user->fresh()); + } +} diff --git a/samooapk/laravel/tests/TestCase.php b/samooapk/laravel/tests/TestCase.php new file mode 100644 index 0000000..2932d4a --- /dev/null +++ b/samooapk/laravel/tests/TestCase.php @@ -0,0 +1,10 @@ +assertTrue(true); + } +} diff --git a/samooapk/laravel/vite.config.js b/samooapk/laravel/vite.config.js new file mode 100644 index 0000000..89f26f5 --- /dev/null +++ b/samooapk/laravel/vite.config.js @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite'; +import laravel from 'laravel-vite-plugin'; + +export default defineConfig({ + plugins: [ + laravel({ + input: [ + 'resources/css/app.css', + 'resources/js/app.js', + ], + refresh: true, + }), + ], +}); diff --git a/samooapk/package-lock.json b/samooapk/package-lock.json new file mode 100644 index 0000000..ccbf0ed --- /dev/null +++ b/samooapk/package-lock.json @@ -0,0 +1,2834 @@ +{ + "name": "samooapk", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@tailwindcss/forms": "^0.5.2", + "alpinejs": "^3.15.1", + "autoprefixer": "^10.4.2", + "axios": "^1.6.4", + "laravel-vite-plugin": "^1.0.0", + "postcss": "^8.4.31", + "tailwindcss": "^3.1.0", + "vite": "^5.0.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", + "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", + "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", + "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", + "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", + "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", + "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", + "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", + "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", + "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", + "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", + "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", + "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", + "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", + "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", + "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", + "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", + "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", + "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", + "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz", + "integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", + "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.1.5" + } + }, + "node_modules/@vue/shared": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", + "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/alpinejs": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.1.tgz", + "integrity": "sha512-HLO1TtiE92VajFHtLLPK8BWaK1YepV/uj31UrfoGnQ00lyFOJZ+oVY3F0DghPAwvg8sLU79pmjGQSytERa2gEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "~3.1.1" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.197", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.197.tgz", + "integrity": "sha512-m1xWB3g7vJ6asIFz+2pBUbq3uGmfmln1M9SSvBe4QIFWYrRHylP73zL/3nMjDmwz8V+1xAXQDfBd6+HPW0WvDQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/laravel-vite-plugin": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.3.0.tgz", + "integrity": "sha512-P5qyG56YbYxM8OuYmK2OkhcKe0AksNVJUjq9LUZ5tOekU9fBn9LujYyctI4t9XoLjuMvHJXXpCoPntY1oKltuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "vite-plugin-full-reload": "^1.1.0" + }, + "bin": { + "clean-orphaned-assets": "bin/clean.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "license": "MIT", + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", + "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.46.2", + "@rollup/rollup-android-arm64": "4.46.2", + "@rollup/rollup-darwin-arm64": "4.46.2", + "@rollup/rollup-darwin-x64": "4.46.2", + "@rollup/rollup-freebsd-arm64": "4.46.2", + "@rollup/rollup-freebsd-x64": "4.46.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", + "@rollup/rollup-linux-arm-musleabihf": "4.46.2", + "@rollup/rollup-linux-arm64-gnu": "4.46.2", + "@rollup/rollup-linux-arm64-musl": "4.46.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", + "@rollup/rollup-linux-ppc64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-musl": "4.46.2", + "@rollup/rollup-linux-s390x-gnu": "4.46.2", + "@rollup/rollup-linux-x64-gnu": "4.46.2", + "@rollup/rollup-linux-x64-musl": "4.46.2", + "@rollup/rollup-win32-arm64-msvc": "4.46.2", + "@rollup/rollup-win32-ia32-msvc": "4.46.2", + "@rollup/rollup-win32-x64-msvc": "4.46.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-full-reload": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz", + "integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "picomatch": "^2.3.1" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + } + } +} diff --git a/samooapk/package.json b/samooapk/package.json new file mode 100644 index 0000000..a6f428b --- /dev/null +++ b/samooapk/package.json @@ -0,0 +1,18 @@ +{ + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "devDependencies": { + "@tailwindcss/forms": "^0.5.2", + "alpinejs": "^3.15.1", + "autoprefixer": "^10.4.2", + "axios": "^1.6.4", + "laravel-vite-plugin": "^1.0.0", + "postcss": "^8.4.31", + "tailwindcss": "^3.1.0", + "vite": "^5.0.0" + } +} diff --git a/samooapk/phpunit.xml b/samooapk/phpunit.xml new file mode 100644 index 0000000..bc86714 --- /dev/null +++ b/samooapk/phpunit.xml @@ -0,0 +1,32 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + diff --git a/samooapk/postcss.config.js b/samooapk/postcss.config.js new file mode 100644 index 0000000..49c0612 --- /dev/null +++ b/samooapk/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/samooapk/public.zip b/samooapk/public.zip new file mode 100644 index 0000000..15a7efe Binary files /dev/null and b/samooapk/public.zip differ diff --git a/samooapk/public/.htaccess b/samooapk/public/.htaccess new file mode 100644 index 0000000..5cefeb4 --- /dev/null +++ b/samooapk/public/.htaccess @@ -0,0 +1,28 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # 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] + + + + + Header set Access-Control-Allow-Origin "*" + Header set Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE" + Header set Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization" + \ No newline at end of file diff --git a/samooapk/public/0XJmCmAXZruutprZOECFWtQyrpeU4brgNVXe631M.jpg b/samooapk/public/0XJmCmAXZruutprZOECFWtQyrpeU4brgNVXe631M.jpg new file mode 100644 index 0000000..07e74a8 Binary files /dev/null and b/samooapk/public/0XJmCmAXZruutprZOECFWtQyrpeU4brgNVXe631M.jpg differ diff --git a/samooapk/public/1jpiAVqs22Q2eTlC5YLWbjJ7JwEOLfhobloacfra.jpg b/samooapk/public/1jpiAVqs22Q2eTlC5YLWbjJ7JwEOLfhobloacfra.jpg new file mode 100644 index 0000000..7ad0c26 Binary files /dev/null and b/samooapk/public/1jpiAVqs22Q2eTlC5YLWbjJ7JwEOLfhobloacfra.jpg differ diff --git a/samooapk/public/2na1FBEm80MKMFanIoID9CMZVDGC7ijEyIKN50FB.jpg b/samooapk/public/2na1FBEm80MKMFanIoID9CMZVDGC7ijEyIKN50FB.jpg new file mode 100644 index 0000000..790424b Binary files /dev/null and b/samooapk/public/2na1FBEm80MKMFanIoID9CMZVDGC7ijEyIKN50FB.jpg differ diff --git a/samooapk/public/3qWVWzL76U1WO2xXHzkP8QIOVguDtVX03j3gOvz9.png b/samooapk/public/3qWVWzL76U1WO2xXHzkP8QIOVguDtVX03j3gOvz9.png new file mode 100644 index 0000000..ad0dfb8 Binary files /dev/null and b/samooapk/public/3qWVWzL76U1WO2xXHzkP8QIOVguDtVX03j3gOvz9.png differ diff --git a/samooapk/public/5OdW5CnDJfzpyrEkZ9l2RJOyfiOZEWbY3Ng6t0Z7.jpg b/samooapk/public/5OdW5CnDJfzpyrEkZ9l2RJOyfiOZEWbY3Ng6t0Z7.jpg new file mode 100644 index 0000000..4cf8632 Binary files /dev/null and b/samooapk/public/5OdW5CnDJfzpyrEkZ9l2RJOyfiOZEWbY3Ng6t0Z7.jpg differ diff --git a/samooapk/public/5l74rh6MEI9EM4jToih9L1wft3BICMEbpQoJMfvq.jpg b/samooapk/public/5l74rh6MEI9EM4jToih9L1wft3BICMEbpQoJMfvq.jpg new file mode 100644 index 0000000..d60498c Binary files /dev/null and b/samooapk/public/5l74rh6MEI9EM4jToih9L1wft3BICMEbpQoJMfvq.jpg differ diff --git a/samooapk/public/BbltGItWvukalyEXBvTDLbVXZTIgXvq0RShe0dcG.jpg b/samooapk/public/BbltGItWvukalyEXBvTDLbVXZTIgXvq0RShe0dcG.jpg new file mode 100644 index 0000000..3ca2a3d Binary files /dev/null and b/samooapk/public/BbltGItWvukalyEXBvTDLbVXZTIgXvq0RShe0dcG.jpg differ diff --git a/samooapk/public/CSS/.htaccess b/samooapk/public/CSS/.htaccess new file mode 100644 index 0000000..8ea3925 --- /dev/null +++ b/samooapk/public/CSS/.htaccess @@ -0,0 +1,27 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # 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] + + + + Header set Access-Control-Allow-Origin "*" + Header set Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE" + Header set Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization" + diff --git a/samooapk/public/CSS/absensi.css b/samooapk/public/CSS/absensi.css new file mode 100644 index 0000000..7f29b96 --- /dev/null +++ b/samooapk/public/CSS/absensi.css @@ -0,0 +1,471 @@ +/* ============================================================ + absensi.css — SIPDAM Absensi Page + Theme: Light Green (sama dengan dashboard) + ============================================================ */ + +@import url('https://fonts.googleapis.com/css2?family=Figtree:wght@400;500;600;700;800&display=swap'); + +:root { + --abs-green: #1a7a4a; + --abs-green-m: #2d9e63; + --abs-green-l: #e8f7ee; + --abs-green-l2: #d0eedd; + --abs-teal: #0d9488; + --abs-blue: #2563eb; + --abs-blue-l: #eff6ff; + --abs-violet: #7c3aed; + --abs-amber: #d97706; + --abs-amber-l: #fffbeb; + --abs-rose: #dc2626; + --abs-rose-l: #fff1f2; + --abs-sky: #0891b2; + --abs-sky-l: #ecfeff; + --abs-border: #d1e8da; + --abs-t1: #0f172a; + --abs-t2: #334155; + --abs-t3: #64748b; + --abs-t4: #94a3b8; + --abs-bg: #f0f7f3; + --abs-white: #ffffff; + --abs-font: 'Figtree', 'Segoe UI', sans-serif; +} + +.abs-icon-wrap { + width: 44px; height: 44px; border-radius: 12px; + background: linear-gradient(135deg, var(--abs-green), var(--abs-green-m)); + display: flex; align-items: center; justify-content: center; + color: #fff; font-size: 18px; + box-shadow: 0 4px 14px rgba(26,122,74,0.25); +} +.abs-page-title { + font-family: var(--abs-font); font-size: 20px; + font-weight: 700; color: var(--abs-t1); line-height: 1; +} +.abs-page-sub { font-size: 12px; color: var(--abs-t3); margin-top: 3px; } + +.abs-wrap { padding: 28px 0; background: var(--abs-bg); min-height: 100vh; } +.abs-inner { max-width: 1400px; margin: 0 auto; padding: 0 24px; } + +/* ── Tab nav ── */ +.abs-tabs { + display: flex; gap: 4px; + background: var(--abs-white); border: 1px solid var(--abs-border); + border-radius: 14px; padding: 5px; margin-bottom: 22px; + box-shadow: 0 1px 4px rgba(0,0,0,0.06); +} +.abs-tab { + flex: 1; padding: 10px 14px; border-radius: 10px; + font-family: var(--abs-font); font-size: 12px; font-weight: 700; + color: var(--abs-t3); border: none; background: transparent; + cursor: pointer; transition: all .2s; + display: flex; align-items: center; justify-content: center; gap: 7px; +} +.abs-tab:hover { color: var(--abs-t2); background: var(--abs-green-l); } +.abs-tab.active { + background: var(--abs-green-l); color: var(--abs-green); + border: 1px solid var(--abs-green-l2); +} +.abs-tab-panel { display: none; } +.abs-tab-panel.active { display: block; } + +/* ── Layout util ── */ +.abs-content-grid { + display: grid; grid-template-columns: 240px 1fr; + gap: 18px; align-items: start; +} +@media(max-width:960px) { .abs-content-grid { grid-template-columns: 1fr; } } + +/* ── Sidebar ── */ +.abs-sidebar { + background: var(--abs-white); border: 1px solid var(--abs-border); + border-radius: 16px; padding: 20px; + position: sticky; top: 20px; + box-shadow: 0 1px 4px rgba(0,0,0,0.06); +} +.abs-sidebar-title { + font-family: var(--abs-font); font-size: 10px; font-weight: 700; + text-transform: uppercase; letter-spacing: .1em; color: var(--abs-t3); + margin-bottom: 16px; display: flex; align-items: center; gap: 7px; +} +.abs-sidebar-title::after { content:''; flex:1; height:1px; background:var(--abs-border); } + +.abs-field + .abs-field { margin-top: 12px; } +.abs-field-label { + font-size: 10px; color: var(--abs-t3); font-weight: 600; + text-transform: uppercase; letter-spacing: .06em; + margin-bottom: 5px; display: block; +} +.abs-input { + width: 100%; background: var(--abs-bg); border: 1px solid var(--abs-border); + color: var(--abs-t1); border-radius: 8px; padding: 9px 12px; + font-family: var(--abs-font); font-size: 12px; outline: none; + transition: border-color .2s; +} +.abs-input:focus { + border-color: var(--abs-green); + box-shadow: 0 0 0 3px rgba(26,122,74,0.08); +} +.abs-input::placeholder { color: var(--abs-t4); } + +.abs-divider { height: 1px; background: var(--abs-border); margin: 14px 0; } + +.abs-chip-group { display: flex; flex-wrap: wrap; gap: 5px; } +.abs-chip { + padding: 4px 10px; border-radius: 99px; font-size: 10px; font-weight: 600; + border: 1px solid var(--abs-border); color: var(--abs-t2); background: transparent; + cursor: pointer; transition: all .2s; font-family: var(--abs-font); +} +.abs-chip:hover, +.abs-chip.active { border-color: var(--abs-green); color: var(--abs-green); background: var(--abs-green-l); } +.abs-chip.green:hover, +.abs-chip.green.active { border-color: var(--abs-green); color: var(--abs-green); background: var(--abs-green-l); } +.abs-chip.amber:hover, +.abs-chip.amber.active { border-color: var(--abs-amber); color: var(--abs-amber); background: var(--abs-amber-l); } +.abs-chip.sky:hover, +.abs-chip.sky.active { border-color: var(--abs-sky); color: var(--abs-sky); background: var(--abs-sky-l); } +.abs-chip.rose:hover, +.abs-chip.rose.active { border-color: var(--abs-rose); color: var(--abs-rose); background: var(--abs-rose-l); } + +.abs-filter-btn { + width: 100%; margin-top: 14px; padding: 10px; border-radius: 9px; + background: linear-gradient(135deg, var(--abs-green), var(--abs-green-m)); + color: #fff; font-family: var(--abs-font); font-size: 11px; font-weight: 700; + border: none; cursor: pointer; + display: flex; align-items: center; justify-content: center; gap: 7px; + transition: all .2s; box-shadow: 0 4px 14px rgba(26,122,74,0.25); +} +.abs-filter-btn:hover { opacity:.9; transform: translateY(-1px); } + +.abs-reset-btn { + width: 100%; margin-top: 7px; padding: 9px; border-radius: 9px; + background: transparent; border: 1px solid var(--abs-border); + color: var(--abs-t2); font-size: 11px; font-family: var(--abs-font); + cursor: pointer; transition: all .2s; + display: flex; align-items: center; justify-content: center; gap: 7px; +} +.abs-reset-btn:hover { border-color: var(--abs-rose); color: var(--abs-rose); } + +/* ── Panel ── */ +.abs-panel { + background: var(--abs-white); border: 1px solid var(--abs-border); + border-radius: 16px; overflow: hidden; + box-shadow: 0 1px 4px rgba(0,0,0,0.06); +} +.abs-panel-head { + padding: 16px 20px; border-bottom: 1px solid var(--abs-border); + display: flex; align-items: center; justify-content: space-between; + background: #f8fdf9; +} +.abs-panel-head-left { + font-family: var(--abs-font); font-size: 12px; + font-weight: 700; color: var(--abs-t1); + display: flex; align-items: center; gap: 7px; +} +.abs-panel-head-right { font-size: 11px; color: var(--abs-t3); } + +/* ── Table ── */ +.abs-table { width: 100%; border-collapse: collapse; } +.abs-table thead tr { background: #f8fdf9; } +.abs-table th { + padding: 12px 18px; text-align: left; + font-family: var(--abs-font); font-size: 9px; font-weight: 700; + letter-spacing: .1em; text-transform: uppercase; color: var(--abs-t3); + border-bottom: 1px solid var(--abs-border); +} +.abs-table th:last-child { text-align: center; } +.abs-table td { + padding: 13px 18px; border-bottom: 1px solid #f0f7f3; + vertical-align: middle; font-family: var(--abs-font); +} +.abs-table tr:last-child td { border-bottom: none; } +.abs-table tbody tr { transition: background .15s; } +.abs-table tbody tr:hover { background: #f8fdf9; } + +.abs-rownum { font-size: 11px; color: var(--abs-t3); font-weight: 600; } +.abs-av { + width: 34px; height: 34px; border-radius: 8px; flex-shrink: 0; + background: linear-gradient(135deg, var(--abs-green), var(--abs-green-m)); + color: #fff; font-size: 10px; font-weight: 700; + display: flex; align-items: center; justify-content: center; +} +.abs-av-name { font-size: 12px; font-weight: 600; color: var(--abs-t1); font-family: var(--abs-font); } +.abs-av-sub { font-size: 10px; color: var(--abs-t3); margin-top: 1px; } +.abs-date { font-size: 11px; color: var(--abs-t1); font-weight: 500; } +.abs-time { font-size: 12px; color: var(--abs-t1); font-weight: 700; } +.abs-time-empty { font-size: 12px; color: var(--abs-t4); } +.abs-duration { font-size: 11px; color: var(--abs-green); font-weight: 600; } + +.abs-late-tag { + display: inline-flex; align-items: center; gap: 3px; + padding: 2px 6px; border-radius: 99px; font-size: 9px; font-weight: 700; + background: var(--abs-rose-l); color: var(--abs-rose); border: 1px solid #fca5a5; +} +.abs-badge { + display: inline-flex; align-items: center; gap: 4px; + padding: 3px 9px; border-radius: 99px; font-size: 10px; font-weight: 700; + font-family: var(--abs-font); border: 1px solid; white-space: nowrap; +} +.abs-badge-green { background: var(--abs-green-l); color: var(--abs-green); border-color: var(--abs-green-l2); } +.abs-badge-amber { background: var(--abs-amber-l); color: var(--abs-amber); border-color: #fde68a; } +.abs-badge-sky { background: var(--abs-sky-l); color: var(--abs-sky); border-color: #a5f3fc; } +.abs-badge-rose { background: var(--abs-rose-l); color: var(--abs-rose); border-color: #fca5a5; } + +.abs-actions { display: flex; align-items: center; justify-content: center; gap: 5px; } +.abs-action-btn { + width: 30px; height: 30px; border-radius: 7px; + border: 1px solid var(--abs-border); background: transparent; + display: flex; align-items: center; justify-content: center; + cursor: pointer; transition: all .2s; font-size: 11px; +} +.abs-action-view { color: var(--abs-green); } +.abs-action-view:hover { background: var(--abs-green-l); border-color: var(--abs-green); } + +.abs-empty { padding: 60px 20px; text-align: center; } +.abs-empty-icon { + width: 60px; height: 60px; border-radius: 14px; + background: var(--abs-green-l); border: 1px solid var(--abs-green-l2); + margin: 0 auto 14px; display: flex; align-items: center; + justify-content: center; font-size: 22px; color: var(--abs-green); +} +.abs-empty-title { + font-family: var(--abs-font); font-size: 13px; + font-weight: 700; color: var(--abs-t2); margin-bottom: 4px; +} +.abs-empty-sub { font-size: 11px; color: var(--abs-t3); } +.abs-pag { + padding: 14px 20px; border-top: 1px solid var(--abs-border); + background: #f8fdf9; +} + +/* ── Riwayat tab ── */ +.ril-controls { + display: flex; align-items: center; gap: 10px; flex-wrap: wrap; + padding: 16px 20px; border-bottom: 1px solid var(--abs-border); + background: #f8fdf9; +} +.ril-select { + background: var(--abs-bg); border: 1px solid var(--abs-border); + color: var(--abs-t1); border-radius: 8px; padding: 8px 12px; + font-size: 12px; font-family: var(--abs-font); + outline: none; cursor: pointer; +} +.ril-select:focus { border-color: var(--abs-green); } + +.ril-nav-btn { + width: 32px; height: 32px; border-radius: 8px; + border: 1px solid var(--abs-border); background: transparent; + color: var(--abs-t2); cursor: pointer; + display: flex; align-items: center; justify-content: center; + font-size: 13px; transition: all .2s; +} +.ril-nav-btn:hover { border-color: var(--abs-green); color: var(--abs-green); } + +.ril-month-label { + font-family: var(--abs-font); font-size: 13px; + font-weight: 700; color: var(--abs-t1); min-width: 120px; text-align: center; +} +.ril-load-btn { + padding: 8px 18px; border-radius: 8px; + background: linear-gradient(135deg, var(--abs-green), var(--abs-green-m)); + color: #fff; font-family: var(--abs-font); font-size: 11px; font-weight: 700; + border: none; cursor: pointer; transition: all .2s; + display: flex; align-items: center; gap: 6px; + box-shadow: 0 4px 14px rgba(26,122,74,0.25); +} +.ril-load-btn:hover { opacity: .9; } + +/* Riwayat list */ +.ril-item { + display: flex; align-items: center; gap: 14px; + padding: 14px 20px; border-bottom: 1px solid #f0f7f3; + transition: background .15s; +} +.ril-item:last-child { border-bottom: none; } +.ril-item:hover { background: #f8fdf9; } + +.ril-date-box { + width: 44px; flex-shrink: 0; text-align: center; + background: var(--abs-green-l); border: 1px solid var(--abs-green-l2); + border-radius: 9px; padding: 8px 4px; +} +.ril-date-num { font-size: 16px; font-weight: 700; color: var(--abs-green); } +.ril-date-day { font-size: 9px; color: var(--abs-t3); font-weight: 600; text-transform: uppercase; margin-top: 1px; } +.ril-times { flex: 1; } +.ril-time-row { font-size: 13px; color: var(--abs-t1); font-weight: 700; } +.ril-time-sub { font-size: 10px; color: var(--abs-t3); margin-top: 3px; } +.ril-sub-green { color: var(--abs-green); } +.ril-sub-amber { color: var(--abs-amber); } + +/* ── Rekap tab ── */ +.rek-layout { display: grid; grid-template-columns: 260px 1fr; gap: 18px; align-items: start; } +@media(max-width:900px) { .rek-layout { grid-template-columns: 1fr; } } + +.rek-sidebar { + background: var(--abs-white); border: 1px solid var(--abs-border); + border-radius: 14px; padding: 18px; + box-shadow: 0 1px 4px rgba(0,0,0,0.06); +} +.rek-sidebar-title { + font-family: var(--abs-font); font-size: 10px; font-weight: 700; + text-transform: uppercase; letter-spacing: .1em; color: var(--abs-t3); + margin-bottom: 14px; display: flex; align-items: center; gap: 7px; +} +.rek-sidebar-title::after { content:''; flex:1; height:1px; background:var(--abs-border); } + +.rek-stat-big { + background: var(--abs-green-l); border: 1px solid var(--abs-green-l2); + border-radius: 16px; padding: 24px; text-align: center; margin-bottom: 14px; +} +.rek-pct-label { font-size: 10px; color: var(--abs-t2); font-weight: 600; letter-spacing: .05em; margin-bottom: 8px; } +.rek-pct-val { font-size: 52px; font-weight: 800; color: var(--abs-green); letter-spacing: -2px; line-height: 1; font-family: var(--abs-font); } +.rek-pct-sub { font-size: 12px; color: var(--abs-t2); margin-top: 6px; } +.rek-progress { height: 6px; background: var(--abs-green-l2); border-radius: 99px; overflow: hidden; margin-top: 14px; } +.rek-progress-bar { height: 100%; background: linear-gradient(90deg, var(--abs-green), var(--abs-green-m)); border-radius: 99px; transition: width .5s; } + +.rek-4grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 10px; margin-bottom: 14px; } +.rek-card { + background: var(--abs-white); border-radius: 12px; padding: 14px 10px; + border: 1px solid; text-align: center; + box-shadow: 0 1px 4px rgba(0,0,0,0.04); +} +.rek-card-num { font-size: 24px; font-weight: 800; font-family: var(--abs-font); } +.rek-card-label { font-size: 9px; font-weight: 600; color: var(--abs-t2); margin-top: 4px; } +.rek-card-bar { height: 3px; border-radius: 99px; margin-top: 8px; } + +.rek-avg { + background: var(--abs-white); border: 1px solid var(--abs-border); + border-radius: 14px; padding: 18px; + box-shadow: 0 1px 4px rgba(0,0,0,0.04); +} +.rek-avg-title { + font-family: var(--abs-font); font-size: 9px; font-weight: 700; + text-transform: uppercase; letter-spacing: .1em; color: var(--abs-t3); + margin-bottom: 14px; display: flex; align-items: center; gap: 7px; +} +.rek-avg-title::after { content:''; flex:1; height:1px; background:var(--abs-border); } + +.rek-avg-row { + display: flex; align-items: center; justify-content: space-between; + padding: 9px 0; border-bottom: 1px solid #f0f7f3; +} +.rek-avg-row:last-child { border-bottom: none; } +.rek-avg-label { font-size: 12px; color: var(--abs-t2); } +.rek-avg-val { font-size: 13px; font-weight: 700; font-family: var(--abs-font); } + +/* ── Modal ── */ +.abs-overlay { + display: none; position: fixed; inset: 0; + background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); + z-index: 50; overflow-y: auto; +} +.abs-overlay.show { display: flex; align-items: flex-start; justify-content: center; padding: 40px 16px; } + +.abs-modal { + background: var(--abs-white); border: 1px solid var(--abs-border); + border-radius: 20px; width: 100%; max-width: 820px; + padding: 32px; box-shadow: 0 24px 60px rgba(0,0,0,0.15); + animation: absModalIn .25s cubic-bezier(.34,1.56,.64,1); +} +@keyframes absModalIn { + from { opacity:0; transform: scale(.94) translateY(12px); } + to { opacity:1; transform: none; } +} +.abs-modal-header { + display: flex; align-items: center; justify-content: space-between; + margin-bottom: 24px; padding-bottom: 18px; border-bottom: 1px solid var(--abs-border); +} +.abs-modal-title { + font-family: var(--abs-font); font-size: 16px; font-weight: 700; + color: var(--abs-t1); display: flex; align-items: center; gap: 9px; +} +.abs-modal-title i { color: var(--abs-green); } +.abs-close-btn { + width: 32px; height: 32px; border-radius: 8px; + border: 1px solid var(--abs-border); color: var(--abs-t3); + background: transparent; cursor: pointer; + display: flex; align-items: center; justify-content: center; + font-size: 12px; transition: all .2s; +} +.abs-close-btn:hover { border-color: var(--abs-rose); color: var(--abs-rose); } + +.abs-detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; } +@media(max-width:600px) { .abs-detail-grid { grid-column: 1fr; } } + +.abs-detail-card { + background: var(--abs-bg); border: 1px solid var(--abs-border); + border-radius: 11px; padding: 14px; +} +.abs-detail-card.full { grid-column: 1/-1; } +.abs-detail-label { font-size: 9px; color: var(--abs-t3); text-transform: uppercase; letter-spacing: .06em; margin-bottom: 4px; } +.abs-detail-val { font-size: 13px; color: var(--abs-t1); font-weight: 600; } +.abs-detail-val-mono { font-size: 15px; color: var(--abs-t1); font-weight: 700; font-family: var(--abs-font); } +.abs-detail-sub { font-size: 10px; color: var(--abs-t3); margin-top: 2px; } + +.abs-keterangan-box { + background: var(--abs-green-l); border: 1px solid var(--abs-green-l2); + border-radius: 11px; padding: 14px; margin-bottom: 12px; +} +.abs-keterangan-label { font-size: 9px; color: var(--abs-green); font-weight: 700; text-transform: uppercase; letter-spacing: .08em; margin-bottom: 6px; } +.abs-keterangan-text { font-size: 12px; color: var(--abs-t1); line-height: 1.6; } + +.abs-foto-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } +@media(max-width:600px) { .abs-foto-grid { grid-template-columns: 1fr; } } + +.abs-foto-card { background: var(--abs-bg); border: 1px solid var(--abs-border); border-radius: 11px; overflow: hidden; } +.abs-foto-card-head { + padding: 9px 13px; border-bottom: 1px solid var(--abs-border); + font-size: 10px; font-weight: 700; color: var(--abs-t2); + font-family: var(--abs-font); text-transform: uppercase; letter-spacing: .06em; + display: flex; align-items: center; gap: 6px; +} +.abs-foto-img { width: 100%; height: 200px; object-fit: cover; display: block; cursor: pointer; transition: transform .2s; } +.abs-foto-img:hover { transform: scale(1.02); } +.abs-foto-empty { + height: 200px; display: flex; flex-direction: column; + align-items: center; justify-content: center; gap: 8px; color: var(--abs-t4); +} +.abs-foto-empty i { font-size: 24px; } +.abs-foto-empty span { font-size: 11px; } + +.abs-spinner { + display: flex; flex-direction: column; align-items: center; + justify-content: center; padding: 50px 20px; gap: 12px; +} +.abs-spinner i { font-size: 24px; color: var(--abs-green); animation: absSpin .8s linear infinite; } +@keyframes absSpin { to { transform: rotate(360deg); } } +.abs-spinner p { font-size: 12px; color: var(--abs-t2); } + +.abs-modal-footer { + display: flex; justify-content: flex-end; + padding-top: 20px; margin-top: 6px; border-top: 1px solid var(--abs-border); +} +.abs-btn-close-footer { + padding: 9px 20px; border-radius: 9px; font-size: 12px; font-weight: 700; + font-family: var(--abs-font); background: transparent; + border: 1px solid var(--abs-border); color: var(--abs-t2); cursor: pointer; + display: flex; align-items: center; gap: 6px; transition: all .2s; +} +.abs-btn-close-footer:hover { border-color: var(--abs-rose); color: var(--abs-rose); } + +.abs-preview-overlay { + display: none; position: fixed; inset: 0; + background: rgba(0,0,0,.93); backdrop-filter: blur(10px); + z-index: 60; align-items: center; justify-content: center; +} +.abs-preview-overlay.show { display: flex; } +.abs-preview-overlay img { max-width: 90vw; max-height: 90vh; object-fit: contain; border-radius: 12px; } +.abs-preview-x { + position: absolute; top: 18px; right: 18px; + width: 38px; height: 38px; border-radius: 50%; + background: var(--abs-rose); color: #fff; border: none; + display: flex; align-items: center; justify-content: center; + font-size: 13px; cursor: pointer; +} + +.ril-loading, .rek-loading { + padding: 50px 20px; text-align: center; color: var(--abs-t2); font-size: 13px; +} +.ril-loading i, .rek-loading i { + font-size: 22px; color: var(--abs-green); animation: absSpin .8s linear infinite; + display: block; margin-bottom: 10px; +} \ No newline at end of file diff --git a/samooapk/public/CSS/akunteknisi.css b/samooapk/public/CSS/akunteknisi.css new file mode 100644 index 0000000..2fbfde7 --- /dev/null +++ b/samooapk/public/CSS/akunteknisi.css @@ -0,0 +1,364 @@ +/* ============================================================ + akunteknisi.css — SIPDAM Akun Teknisi Page + Theme: Light Green (sama dengan dashboard) + ============================================================ */ + +@import url('https://fonts.googleapis.com/css2?family=Figtree:wght@400;500;600;700;800&display=swap'); + +:root { + --akt-green: #1a7a4a; + --akt-green-m: #2d9e63; + --akt-green-l: #e8f7ee; + --akt-green-l2: #d0eedd; + --akt-teal: #0d9488; + --akt-blue: #2563eb; + --akt-blue-l: #eff6ff; + --akt-violet: #7c3aed; + --akt-violet-l: #f5f3ff; + --akt-amber: #d97706; + --akt-amber-l: #fffbeb; + --akt-rose: #dc2626; + --akt-rose-l: #fff1f2; + --akt-cyan: #0891b2; + --akt-cyan-l: #ecfeff; + --akt-border: #d1e8da; + --akt-t1: #0f172a; + --akt-t2: #334155; + --akt-t3: #64748b; + --akt-t4: #94a3b8; + --akt-bg: #f0f7f3; + --akt-white: #ffffff; + --akt-font: 'Figtree', 'Segoe UI', sans-serif; +} + +/* ── Header ── */ +.akt-icon-wrap { + width: 44px; height: 44px; border-radius: 12px; + background: linear-gradient(135deg, var(--akt-green), var(--akt-green-m)); + display: flex; align-items: center; justify-content: center; + color: #fff; font-size: 18px; + box-shadow: 0 4px 14px rgba(26,122,74,0.25); +} +.akt-page-title { + font-family: var(--akt-font); font-size: 20px; + font-weight: 700; color: var(--akt-t1); line-height: 1; +} +.akt-page-sub { font-size: 12px; color: var(--akt-t3); margin-top: 3px; } + +.akt-btn-primary { + font-family: var(--akt-font); + background: linear-gradient(135deg, var(--akt-green), var(--akt-green-m)); + color: #fff; font-weight: 700; font-size: 13px; + padding: 10px 20px; border-radius: 10px; border: none; + display: flex; align-items: center; gap: 8px; + cursor: pointer; transition: all .2s; + box-shadow: 0 4px 14px rgba(26,122,74,0.25); +} +.akt-btn-primary:hover { transform: translateY(-2px); opacity: .9; } + +/* ── Wrap ── */ +.akt-wrap { padding: 28px 0; background: var(--akt-bg); min-height: 100vh; } +.akt-inner { max-width: 1300px; margin: 0 auto; padding: 0 24px; } + +/* ── Alert ── */ +.akt-alert { + padding: 14px 18px; border-radius: 12px; + display: flex; align-items: center; gap: 10px; + margin-bottom: 24px; font-size: 14px; + font-family: var(--akt-font); + animation: akt-fadeIn .3s ease; +} +.akt-alert-ok { + background: var(--akt-green-l); border: 1px solid var(--akt-green-l2); + color: var(--akt-green); +} +.akt-alert-err { + background: var(--akt-rose-l); border: 1px solid #fca5a5; + color: var(--akt-rose); +} +@keyframes akt-fadeIn { + from { opacity:0; transform:translateY(-8px); } + to { opacity:1; transform:none; } +} + +/* ── Toolbar ── */ +.akt-toolbar { + display: flex; align-items: center; gap: 14px; + flex-wrap: wrap; margin-bottom: 20px; +} +.akt-search-wrap { flex: 1; min-width: 220px; position: relative; } +.akt-search-wrap i { + position: absolute; left: 14px; top: 50%; + transform: translateY(-50%); color: var(--akt-t4); + font-size: 13px; pointer-events: none; +} +.akt-search-input { + width: 100%; background: var(--akt-white); + border: 1px solid var(--akt-border); color: var(--akt-t1); + border-radius: 10px; padding: 11px 14px 11px 38px; + font-family: var(--akt-font); font-size: 13px; + outline: none; transition: border-color .2s; +} +.akt-search-input::placeholder { color: var(--akt-t4); } +.akt-search-input:focus { + border-color: var(--akt-green); + box-shadow: 0 0 0 3px rgba(26,122,74,0.08); +} + +.akt-chip-row { display: flex; gap: 7px; flex-wrap: wrap; } +.akt-chip { + padding: 8px 16px; border-radius: 10px; + font-size: 12px; font-weight: 700; font-family: var(--akt-font); + border: 1px solid var(--akt-border); color: var(--akt-t2); + background: var(--akt-white); cursor: pointer; transition: all .2s; + display: flex; align-items: center; gap: 7px; +} +.akt-chip .cnt { + background: #f1f5f9; padding: 2px 8px; + border-radius: 99px; font-size: 11px; +} +.akt-chip:hover { border-color: var(--akt-green); color: var(--akt-green); background: var(--akt-green-l); } +.akt-chip.active { border-color: var(--akt-green); color: var(--akt-green); background: var(--akt-green-l); } +.akt-chip.active .cnt { background: var(--akt-green-l2); } +.akt-chip.green:hover, +.akt-chip.green.active { border-color: var(--akt-green); color: var(--akt-green); background: var(--akt-green-l); } +.akt-chip.green.active .cnt { background: var(--akt-green-l2); } +.akt-chip.rose:hover, +.akt-chip.rose.active { border-color: var(--akt-rose); color: var(--akt-rose); background: var(--akt-rose-l); } + +/* ── Panel ── */ +.akt-panel { + background: var(--akt-white); border: 1px solid var(--akt-border); + border-radius: 16px; overflow: hidden; + box-shadow: 0 1px 4px rgba(0,0,0,0.06); +} +.akt-panel-head { + padding: 16px 24px; border-bottom: 1px solid var(--akt-border); + display: flex; align-items: center; justify-content: space-between; + background: #f8fdf9; +} +.akt-panel-head-left { + font-family: var(--akt-font); font-size: 13px; + font-weight: 700; color: var(--akt-t1); + display: flex; align-items: center; gap: 8px; +} +.akt-panel-head-right { font-size: 12px; color: var(--akt-t3); } + +/* ── Table ── */ +.akt-table { width: 100%; border-collapse: collapse; } +.akt-table thead tr { background: #f8fdf9; } +.akt-table th { + padding: 13px 20px; text-align: left; + font-family: var(--akt-font); font-size: 10px; font-weight: 700; + letter-spacing: .08em; text-transform: uppercase; color: var(--akt-t3); + border-bottom: 1px solid var(--akt-border); +} +.akt-table th:last-child { text-align: center; } +.akt-table td { + padding: 14px 20px; border-bottom: 1px solid #f0f7f3; + vertical-align: middle; font-family: var(--akt-font); + font-size: 13px; color: var(--akt-t2); +} +.akt-table tr:last-child td { border-bottom: none; } +.akt-table tbody tr.table-row { transition: background .15s; } +.akt-table tbody tr.table-row:hover { background: #f8fdf9; } + +.akt-rownum { + width: 28px; height: 28px; border-radius: 8px; + background: var(--akt-green-l); border: 1px solid var(--akt-green-l2); + color: var(--akt-green); font-size: 11px; font-weight: 700; + display: inline-flex; align-items: center; justify-content: center; +} + +/* Teknisi cell */ +.akt-av { + width: 38px; height: 38px; border-radius: 10px; flex-shrink: 0; + background: linear-gradient(135deg, var(--akt-green), var(--akt-green-m)); + color: #fff; font-size: 13px; font-weight: 700; + display: flex; align-items: center; justify-content: center; +} +.akt-av-name { + font-size: 13px; font-weight: 600; + color: var(--akt-t1); font-family: var(--akt-font); +} +.akt-av-sub { font-size: 11px; color: var(--akt-t3); margin-top: 2px; } + +/* Username */ +.akt-username { + display: inline-flex; align-items: center; gap: 7px; + background: var(--akt-green-l); border: 1px solid var(--akt-green-l2); + border-radius: 8px; padding: 6px 12px; + font-size: 12px; color: var(--akt-green); + font-family: var(--akt-font); font-weight: 600; +} +.akt-username i { font-size: 11px; } + +/* Status badges */ +.akt-badge { + display: inline-flex; align-items: center; gap: 5px; + padding: 4px 10px; border-radius: 99px; + font-size: 11px; font-weight: 700; + font-family: var(--akt-font); border: 1px solid; +} +.akt-badge-green { background: var(--akt-green-l); color: var(--akt-green); border-color: var(--akt-green-l2); } +.akt-badge-rose { background: var(--akt-rose-l); color: var(--akt-rose); border-color: #fca5a5; } + +/* Toggle btn */ +.akt-toggle-btn { + width: 30px; height: 30px; border-radius: 7px; + border: 1px solid var(--akt-border); background: transparent; + color: var(--akt-t3); cursor: pointer; font-size: 12px; + display: inline-flex; align-items: center; justify-content: center; + transition: all .2s; margin-left: 8px; +} +.akt-toggle-btn:hover { + border-color: var(--akt-green); color: var(--akt-green); + background: var(--akt-green-l); +} + +/* Action buttons */ +.akt-actions { display: flex; align-items: center; justify-content: center; gap: 6px; } +.akt-action-btn { + width: 32px; height: 32px; border-radius: 8px; + border: 1px solid var(--akt-border); background: transparent; + display: flex; align-items: center; justify-content: center; + cursor: pointer; transition: all .2s; font-size: 12px; +} +.akt-action-edit { color: var(--akt-teal); } +.akt-action-edit:hover { background: var(--akt-cyan-l); border-color: var(--akt-teal); } +.akt-action-del { color: var(--akt-rose); } +.akt-action-del:hover { background: var(--akt-rose-l); border-color: var(--akt-rose); } + +/* Empty / No result */ +.akt-empty { padding: 72px 20px; text-align: center; } +.akt-empty-icon { + width: 68px; height: 68px; border-radius: 16px; + background: var(--akt-green-l); border: 1px solid var(--akt-green-l2); + margin: 0 auto 18px; display: flex; align-items: center; justify-content: center; + font-size: 26px; color: var(--akt-green); +} +.akt-empty-title { + font-family: var(--akt-font); font-size: 14px; + font-weight: 700; color: var(--akt-t2); margin-bottom: 5px; +} +.akt-empty-sub { font-size: 12px; color: var(--akt-t3); } + +/* ── Modals ── */ +.akt-overlay { + display: none; position: fixed; inset: 0; + background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); + z-index: 50; overflow-y: auto; +} +.akt-overlay.show { + display: flex; align-items: flex-start; + justify-content: center; padding: 40px 16px; +} +.akt-modal { + background: var(--akt-white); border: 1px solid var(--akt-border); + border-radius: 20px; width: 100%; max-width: 540px; + box-shadow: 0 24px 60px rgba(0,0,0,0.15); + animation: akt-modalIn .25s cubic-bezier(.34,1.56,.64,1); + overflow: hidden; +} +@keyframes akt-modalIn { + from { opacity:0; transform: scale(.94) translateY(12px); } + to { opacity:1; transform: none; } +} + +.akt-modal-stripe { height: 3px; } +.akt-modal-stripe-green { + background: linear-gradient(90deg, var(--akt-green), var(--akt-green-m), var(--akt-teal)); +} +.akt-modal-stripe-cyan { + background: linear-gradient(90deg, var(--akt-teal), var(--akt-cyan)); +} + +.akt-modal-inner { padding: 28px 32px 32px; } + +.akt-modal-header { + display: flex; align-items: center; justify-content: space-between; + margin-bottom: 26px; padding-bottom: 18px; + border-bottom: 1px solid var(--akt-border); +} +.akt-modal-title { + font-family: var(--akt-font); font-size: 17px; + font-weight: 700; color: var(--akt-t1); + display: flex; align-items: center; gap: 10px; +} +.akt-modal-title i { font-size: 15px; color: var(--akt-green); } +.akt-close-btn { + width: 32px; height: 32px; border-radius: 8px; + border: 1px solid var(--akt-border); color: var(--akt-t3); + background: transparent; cursor: pointer; + display: flex; align-items: center; justify-content: center; + font-size: 12px; transition: all .2s; +} +.akt-close-btn:hover { border-color: var(--akt-rose); color: var(--akt-rose); } + +/* Form */ +.akt-form-fields { display: flex; flex-direction: column; gap: 18px; } +.akt-form-field { display: flex; flex-direction: column; gap: 7px; } +.akt-form-label { + font-size: 11px; font-weight: 700; letter-spacing: .06em; + text-transform: uppercase; color: var(--akt-t2); + display: flex; align-items: center; gap: 6px; +} +.akt-form-label i { font-size: 10px; color: var(--akt-green); } +.akt-form-label .req { color: var(--akt-rose); } +.akt-form-label .opt { color: var(--akt-t4); font-weight: 400; text-transform: none; letter-spacing: 0; font-size: 10px; } + +.akt-select, .akt-text-input, .akt-pw-input { + background: var(--akt-bg); border: 1px solid var(--akt-border); + color: var(--akt-t1); border-radius: 10px; padding: 11px 14px; + font-family: var(--akt-font); font-size: 13px; + outline: none; transition: border-color .2s, box-shadow .2s; width: 100%; +} +.akt-select:focus, .akt-text-input:focus, .akt-pw-input:focus { + border-color: var(--akt-green); + box-shadow: 0 0 0 3px rgba(26,122,74,0.08); +} +.akt-text-input::placeholder, .akt-pw-input::placeholder { color: var(--akt-t4); } +.akt-select.disabled-field { + background: #f1f5f9; color: var(--akt-t4); cursor: not-allowed; +} + +.akt-hint { font-size: 11px; color: var(--akt-t3); margin-top: 2px; } +.akt-err { font-size: 11px; color: var(--akt-rose); margin-top: 2px; min-height: 14px; } + +.akt-pw-wrap { position: relative; } +.akt-pw-wrap .akt-pw-input { padding-right: 42px; } +.akt-pw-toggle { + position: absolute; right: 12px; top: 50%; transform: translateY(-50%); + background: none; border: none; color: var(--akt-t3); + cursor: pointer; font-size: 13px; transition: color .2s; +} +.akt-pw-toggle:hover { color: var(--akt-green); } + +.akt-modal-footer { + display: flex; align-items: center; justify-content: flex-end; + gap: 10px; padding-top: 22px; margin-top: 6px; + border-top: 1px solid var(--akt-border); +} +.akt-btn-cancel { + padding: 10px 20px; border-radius: 10px; font-size: 13px; font-weight: 600; + font-family: var(--akt-font); background: transparent; + border: 1px solid var(--akt-border); color: var(--akt-t2); + cursor: pointer; display: flex; align-items: center; gap: 7px; transition: all .2s; +} +.akt-btn-cancel:hover { border-color: var(--akt-rose); color: var(--akt-rose); } +.akt-btn-submit { + padding: 10px 24px; border-radius: 10px; font-size: 13px; font-weight: 700; + font-family: var(--akt-font); border: none; cursor: pointer; + display: flex; align-items: center; gap: 7px; transition: all .2s; +} +.akt-btn-submit-green { + background: linear-gradient(135deg, var(--akt-green), var(--akt-green-m)); + color: #fff; box-shadow: 0 4px 14px rgba(26,122,74,0.25); +} +.akt-btn-submit-cyan { + background: linear-gradient(135deg, var(--akt-teal), var(--akt-cyan)); + color: #fff; box-shadow: 0 4px 14px rgba(13,148,136,0.25); +} +.akt-btn-submit:hover { opacity: .9; transform: translateY(-1px); } + +#akt-alert-msg { transition: opacity .4s; } \ No newline at end of file diff --git a/samooapk/public/CSS/app.css b/samooapk/public/CSS/app.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/samooapk/public/CSS/app.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/samooapk/public/CSS/dashboard.css b/samooapk/public/CSS/dashboard.css new file mode 100644 index 0000000..489d9bf --- /dev/null +++ b/samooapk/public/CSS/dashboard.css @@ -0,0 +1,589 @@ +/* ============================================================ + dashboard.css — PERUMDA Tirta Sanjiwani + Letakkan di: public/css/dashboard.css + Lalu panggil di layout: + ============================================================ */ + +@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap'); + +*, *::before, *::after { + box-sizing: border-box; +} + +/* ── WRAPPER ── */ +.db-wrap { + font-family: 'Plus Jakarta Sans', sans-serif; + padding: 1.5rem; + background: #f4f6f8; + min-height: 100vh; +} + +/* ══════════════════════════════════════ + BANNER + ══════════════════════════════════════ */ +.db-banner { + background: linear-gradient(135deg, #0f6e56 0%, #1d9e75 50%, #3ab574 100%); + border-radius: 16px; + padding: 1.25rem 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 1.25rem; +} + +.db-banner-left { + display: flex; + align-items: center; + gap: 14px; +} + +.db-av { + width: 50px; + height: 50px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.25); + border: 2px solid rgba(255, 255, 255, 0.4); + display: flex; + align-items: center; + justify-content: center; + font-size: 21px; + font-weight: 700; + color: #fff; +} + +.db-av-name { + font-size: 17px; + font-weight: 700; + color: #fff; +} + +.db-av-email { + font-size: 12px; + color: rgba(255, 255, 255, 0.75); + margin-top: 2px; +} + +.db-chips { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.db-chip { + background: rgba(255, 255, 255, 0.18); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 20px; + padding: 5px 12px; + font-size: 11px; + font-weight: 500; + color: #fff; + display: flex; + align-items: center; + gap: 6px; +} + +.chip-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: #4ade80; + display: inline-block; +} + +/* ══════════════════════════════════════ + STAT CARDS + ══════════════════════════════════════ */ +.db-stats { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; + margin-bottom: 1.25rem; +} + +@media (max-width: 900px) { + .db-stats { grid-template-columns: repeat(2, 1fr); } +} + +@media (max-width: 560px) { + .db-stats { grid-template-columns: 1fr; } +} + +.db-stat { + border-radius: 14px; + padding: 1.1rem 1.2rem; + position: relative; + overflow: hidden; +} + +.db-stat-ico { + width: 40px; + height: 40px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 12px; +} + +.db-stat-ico svg { + width: 20px; + height: 20px; +} + +.db-stat-lbl { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.6px; + opacity: 0.7; + margin-bottom: 5px; +} + +.db-stat-num { + font-size: 30px; + font-weight: 700; + line-height: 1; + margin-bottom: 5px; +} + +.db-stat-sub { + font-size: 11px; + opacity: 0.65; +} + +.db-stat-deco { + position: absolute; + right: -12px; + top: -12px; + width: 75px; + height: 75px; + border-radius: 50%; + opacity: 0.13; +} + +/* Warna stat cards */ +.s-green { background: #e1f5ee; color: #085041; } +.s-green .db-stat-ico { background: #9fe1cb; } +.s-green .db-stat-deco { background: #1d9e75; } + +.s-blue { background: #e6f1fb; color: #042c53; } +.s-blue .db-stat-ico { background: #b5d4f4; } +.s-blue .db-stat-deco { background: #378add; } + +.s-amber { background: #faeeda; color: #412402; } +.s-amber .db-stat-ico { background: #fac775; } +.s-amber .db-stat-deco { background: #ef9f27; } + +.s-violet { background: #eeedfe; color: #26215c; } +.s-violet .db-stat-ico { background: #cecbf6; } +.s-violet .db-stat-deco { background: #7f77dd; } + +/* ══════════════════════════════════════ + CHART BAR WRAPPER + ══════════════════════════════════════ */ +.db-chart-wrap { + background: #fff; + border: 1px solid #eee; + border-radius: 14px; + padding: 1.2rem; + margin-bottom: 1.25rem; +} + +.db-bar-legend { + display: flex; + gap: 14px; + flex-wrap: wrap; + margin-bottom: 12px; +} + +/* ══════════════════════════════════════ + MAIN 2-COL GRID + ══════════════════════════════════════ */ +.db-main { + display: grid; + grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr); + gap: 12px; +} + +@media (max-width: 900px) { + .db-main { grid-template-columns: 1fr; } +} + +/* ══════════════════════════════════════ + PANELS (kartu putih umum) + ══════════════════════════════════════ */ +.db-panel { + background: #fff; + border: 1px solid #eee; + border-radius: 14px; + padding: 1.2rem; + margin-bottom: 12px; +} + +.db-panel:last-child { + margin-bottom: 0; +} + +.db-panel-head { + display: flex; + align-items: center; + gap: 8px; + padding-bottom: 0.85rem; + margin-bottom: 1rem; + border-bottom: 1px solid #f0f0f0; +} + +.db-panel-head-dot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.db-panel-head-title { + font-size: 13px; + font-weight: 600; + color: #1a1a1a; +} + +/* ══════════════════════════════════════ + QUICK LINKS + ══════════════════════════════════════ */ +.db-qlink { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 8px; + border-radius: 10px; + text-decoration: none; + transition: background 0.15s; + margin-bottom: 3px; +} + +.db-qlink:hover { + background: #f5f5f5; +} + +.db-qlink-ico { + width: 38px; + height: 38px; + border-radius: 9px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.db-qlink-ico svg { + width: 17px; + height: 17px; +} + +.db-qlink-title { + font-size: 13px; + font-weight: 600; + color: #1a1a1a; +} + +.db-qlink-sub { + font-size: 11px; + color: #888; + margin-top: 2px; +} + +.db-qlink-arr { + margin-left: auto; + color: #bbb; + font-size: 18px; +} + +/* ══════════════════════════════════════ + TARIF TABLE + ══════════════════════════════════════ */ +.db-tarif-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; +} + +.db-tarif-table th { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: #999; + padding: 6px 8px; + border-bottom: 1px solid #f0f0f0; + text-align: left; +} + +.db-tarif-table td { + padding: 7px 8px; + border-bottom: 1px solid #f7f7f7; + color: #1a1a1a; + vertical-align: middle; +} + +.db-tarif-table tr:last-child td { + border-bottom: none; +} + +.db-tarif-table tr:hover td { + background: #fafafa; +} + +.db-tarif-price { + font-weight: 700; +} + +/* Badge jenis pekerjaan */ +.badge-jenis { + display: inline-block; + padding: 3px 9px; + border-radius: 20px; + font-size: 10px; + font-weight: 600; +} + +.bj-sr { background: #e1f5ee; color: #085041; } +.bj-pjp { background: #e6f1fb; color: #042c53; } +.bj-perbaikan { background: #fcebeb; color: #501313; } +.bj-gali { background: #faeeda; color: #412402; } +.bj-gv { background: #eeedfe; color: #26215c; } +.bj-cat { background: #fbeaf0; color: #4b1528; } + +/* ══════════════════════════════════════ + KOLOM KANAN + ══════════════════════════════════════ */ +.db-right { + display: flex; + flex-direction: column; + gap: 12px; +} + +/* ── STATUS SISTEM ── */ +.db-sys { + background: #fff; + border: 1px solid #eee; + border-radius: 14px; + padding: 1.2rem; +} + +.db-sys-title { + font-size: 13px; + font-weight: 600; + color: #1a1a1a; + display: flex; + align-items: center; + gap: 8px; + padding-bottom: 0.85rem; + margin-bottom: 0.85rem; + border-bottom: 1px solid #f0f0f0; +} + +.db-sys-title svg { + width: 15px; + height: 15px; + color: #888; +} + +.db-sys-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 7px 0; + border-bottom: 1px solid #f7f7f7; +} + +.db-sys-row:last-child { + border-bottom: none; +} + +.db-sys-lbl { + font-size: 12px; + color: #666; + display: flex; + align-items: center; + gap: 7px; +} + +.db-sys-lbl svg { + width: 13px; + height: 13px; +} + +/* Badge status */ +.db-badge { + font-size: 10px; + font-weight: 700; + padding: 3px 10px; + border-radius: 20px; +} + +.db-badge-ok { background: #e1f5ee; color: #0f6e56; } +.db-badge-am { background: #faeeda; color: #633806; } +.db-badge-num { background: #e6f1fb; color: #042c53; } + +/* ── PERINGATAN ── */ +.db-warn { + background: #fff; + border: 1px solid #eee; + border-radius: 14px; + padding: 1.2rem; +} + +.db-warn-head { + display: flex; + align-items: center; + gap: 8px; + padding-bottom: 0.85rem; + margin-bottom: 1rem; + border-bottom: 1px solid #f0f0f0; +} + +.db-warn-box { + background: #fffbeb; + border: 1px solid #fac775; + border-radius: 10px; + padding: 0.9rem; +} + +.db-warn-box-title { + font-size: 12px; + font-weight: 700; + color: #633806; + display: flex; + align-items: center; + gap: 7px; + margin-bottom: 8px; +} + +.db-warn-box-title svg { + width: 14px; + height: 14px; + color: #d97706; +} + +.db-warn-tag { + background: #fac775; + color: #412402; + font-size: 10px; + font-weight: 700; + padding: 2px 8px; + border-radius: 20px; + margin-left: auto; +} + +.db-warn-row { + display: flex; + justify-content: space-between; + font-size: 12px; + color: #633806; + padding: 5px 0; + border-bottom: 1px solid #fde68a; +} + +.db-warn-row:last-child { + border-bottom: none; +} + +.db-warn-date { + font-size: 11px; + opacity: 0.7; +} + +.db-empty { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: #999; + justify-content: center; + padding: 0.75rem; +} + +.db-empty svg { + width: 16px; + height: 16px; +} + +/* ── INFO CARD PERUMDA ── */ +.db-info-card { + background: linear-gradient(145deg, #04342c, #0f6e56); + border-radius: 14px; + padding: 1.2rem; + color: #fff; +} + +.db-info-logo { + font-size: 10px; + font-weight: 700; + letter-spacing: 1px; + text-transform: uppercase; + opacity: 0.65; + margin-bottom: 4px; +} + +.db-info-name { + font-size: 19px; + font-weight: 700; + margin-bottom: 3px; +} + +.db-info-region { + font-size: 12px; + opacity: 0.7; + margin-bottom: 0.85rem; +} + +.db-info-divider { + border: none; + border-top: 1px solid rgba(255, 255, 255, 0.18); + margin-bottom: 0.85rem; +} + +.db-info-row { + font-size: 12px; + opacity: 0.8; + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 6px; +} + +.db-info-row svg { + width: 13px; + height: 13px; + opacity: 0.7; + flex-shrink: 0; +} + +/* ── DONUT CARD ── */ +.db-donut-card { + background: #fff; + border: 1px solid #eee; + border-radius: 14px; + padding: 1.2rem; +} + +.db-chart-legend { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-top: 10px; +} + +.db-legend-item { + display: flex; + align-items: center; + gap: 5px; + font-size: 11px; + color: #666; +} + +.db-legend-sq { + width: 10px; + height: 10px; + border-radius: 2px; +} \ No newline at end of file diff --git a/samooapk/public/CSS/index.php b/samooapk/public/CSS/index.php new file mode 100644 index 0000000..9b9f003 --- /dev/null +++ b/samooapk/public/CSS/index.php @@ -0,0 +1,55 @@ +make(Kernel::class); + +$response = $kernel->handle( + $request = Request::capture() +)->send(); + +$kernel->terminate($request, $response); diff --git a/samooapk/public/CSS/kasbon.css b/samooapk/public/CSS/kasbon.css new file mode 100644 index 0000000..fe3ee64 --- /dev/null +++ b/samooapk/public/CSS/kasbon.css @@ -0,0 +1,42 @@ +.card { + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); + border: 1px solid rgba(0, 0, 0, 0.125); +} + +.table th { + background-color: #f8f9fa; + font-weight: 600; + border-bottom: 2px solid #dee2e6; +} + +.badge { + font-size: 0.75em; +} + +.btn-sm { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; +} + +.search-box { + max-width: 300px; +} + +.status-lunas { + background-color: #28a745; + color: white; +} + +.status-belum-lunas { + background-color: #dc3545; + color: white; +} + +.action-buttons { + white-space: nowrap; +} + +.currency { + text-align: right; + font-weight: 500; +} diff --git a/samooapk/public/CSS/kelolaadmin.css b/samooapk/public/CSS/kelolaadmin.css new file mode 100644 index 0000000..8c1b6ca --- /dev/null +++ b/samooapk/public/CSS/kelolaadmin.css @@ -0,0 +1,277 @@ +.kadm-wrap { padding: 0; } +.kadm-inner { max-width: 1200px; margin: 0 auto; } + +/* PAGE HEADER */ +.kadm-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 28px; + flex-wrap: wrap; + gap: 16px; +} +.kadm-header-left { display: flex; align-items: center; gap: 16px; } +.kadm-icon-wrap { + width: 52px; height: 52px; + background: linear-gradient(135deg, #16a34a, #15803d); + border-radius: 14px; + display: flex; align-items: center; justify-content: center; + color: white; font-size: 22px; + box-shadow: 0 4px 12px rgba(22,163,74,0.3); +} +.kadm-page-title { font-size: 22px; font-weight: 800; color: #1e293b; margin: 0 0 4px; } +.kadm-page-sub { font-size: 13px; color: #64748b; margin: 0; } + +/* ALERT */ +.kadm-alert { + padding: 14px 18px; + border-radius: 12px; + margin-bottom: 20px; + display: flex; + align-items: center; + gap: 10px; + font-size: 14px; + font-weight: 600; + animation: slideDown 0.3s ease; +} +.kadm-alert-ok { background: #f0fdf4; border: 1px solid #bbf7d0; color: #15803d; } +.kadm-alert-err { background: #fef2f2; border: 1px solid #fecaca; color: #dc2626; } +.kadm-alert.hidden { display: none; } +@keyframes slideDown { from { opacity:0; transform:translateY(-8px); } to { opacity:1; transform:translateY(0); } } + +/* NOTICE BOX */ +.kadm-notice { + background: #fffbeb; + border: 1px solid #fde68a; + border-radius: 12px; + padding: 14px 18px; + margin-bottom: 24px; + font-size: 13px; + color: #92400e; + display: flex; + align-items: center; + gap: 10px; +} + +/* PANEL */ +.kadm-panel { + background: #fff; + border-radius: 18px; + border: 1px solid #f1f5f9; + box-shadow: 0 2px 12px rgba(0,0,0,0.05); + overflow: hidden; +} +.kadm-panel-head { + padding: 18px 24px; + border-bottom: 1px solid #f1f5f9; + display: flex; + align-items: center; + justify-content: space-between; + background: #f8fafc; +} +.kadm-panel-head-left { + font-size: 13px; + font-weight: 700; + color: #64748b; + letter-spacing: 0.5px; + text-transform: uppercase; + display: flex; + align-items: center; + gap: 8px; +} +.kadm-panel-head-right { + font-size: 12px; + color: #94a3b8; +} + +/* TABLE */ +.kadm-table-wrap { overflow-x: auto; } +.kadm-table { width: 100%; border-collapse: collapse; } +.kadm-table thead tr { background: #f8fafc; } +.kadm-table th { + padding: 14px 20px; + font-size: 11px; + font-weight: 700; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; + text-align: left; + border-bottom: 1px solid #f1f5f9; +} +.kadm-table td { + padding: 16px 20px; + border-bottom: 1px solid #f8fafc; + vertical-align: middle; +} +.kadm-table tbody tr:last-child td { border-bottom: none; } +.kadm-table tbody tr:hover { background: #fafcff; } + +/* AVATAR */ +.kadm-av { + width: 40px; height: 40px; + border-radius: 12px; + display: flex; align-items: center; justify-content: center; + font-weight: 800; font-size: 16px; + background: linear-gradient(135deg, #dcfce7, #bbf7d0); + color: #16a34a; + flex-shrink: 0; +} +.kadm-av-name { font-weight: 700; font-size: 14px; color: #1e293b; } +.kadm-av-email { font-size: 12px; color: #94a3b8; margin-top: 2px; } + +/* BADGE */ +.kadm-badge { + display: inline-flex; align-items: center; gap: 5px; + padding: 4px 12px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; +} +.kadm-badge-super { background: #f3e8ff; color: #7c3aed; } +.kadm-badge-admin { background: #dbeafe; color: #1d4ed8; } + +/* ACTIONS */ +.kadm-actions { display: flex; align-items: center; gap: 6px; } +.kadm-btn { + width: 34px; height: 34px; + border-radius: 8px; + border: none; + cursor: pointer; + display: flex; align-items: center; justify-content: center; + font-size: 13px; + transition: all 0.2s; +} +.kadm-btn:hover { transform: scale(1.08); } +.kadm-btn-view { background: #f0fdf4; color: #16a34a; } +.kadm-btn-edit { background: #eff6ff; color: #3b82f6; } +.kadm-btn-del { background: #fef2f2; color: #ef4444; } +.kadm-btn-del:disabled { background: #f1f5f9; color: #cbd5e1; cursor: not-allowed; transform: none; } + +/* EMPTY STATE */ +.kadm-empty { + text-align: center; + padding: 60px 20px; + color: #94a3b8; +} +.kadm-empty i { font-size: 48px; margin-bottom: 12px; opacity: 0.4; } +.kadm-empty-title { font-size: 16px; font-weight: 700; color: #64748b; margin-bottom: 6px; } + +/* MODAL OVERLAY */ +.kadm-overlay { + position: fixed; inset: 0; + background: rgba(0,0,0,0.45); + z-index: 9999; + display: none; + align-items: center; + justify-content: center; + padding: 20px; +} +.kadm-overlay.show { display: flex; } +.kadm-modal { + background: #fff; + border-radius: 20px; + width: 100%; max-width: 500px; + box-shadow: 0 25px 60px rgba(0,0,0,0.2); + animation: modalIn 0.25s ease; + overflow: hidden; +} +@keyframes modalIn { from { opacity:0; transform: scale(0.94) translateY(10px); } to { opacity:1; transform: scale(1) translateY(0); } } +.kadm-modal-header { + padding: 20px 24px; + border-bottom: 1px solid #f1f5f9; + display: flex; align-items: center; justify-content: space-between; +} +.kadm-modal-title { font-size: 16px; font-weight: 800; color: #1e293b; display: flex; align-items: center; gap: 10px; } +.kadm-modal-title i { color: #16a34a; } +.kadm-close { background: #f1f5f9; border: none; width: 32px; height: 32px; border-radius: 8px; cursor: pointer; font-size: 14px; color: #64748b; } +.kadm-close:hover { background: #e2e8f0; } +.kadm-modal-body { padding: 24px; } +.kadm-modal-footer { + padding: 16px 24px; + border-top: 1px solid #f1f5f9; + display: flex; gap: 10px; justify-content: flex-end; +} + +/* FORM */ +.kadm-form-group { margin-bottom: 18px; } +.kadm-form-group:last-child { margin-bottom: 0; } +.kadm-label { + display: block; + font-size: 13px; font-weight: 700; color: #374151; + margin-bottom: 8px; +} +.kadm-input { + width: 100%; + padding: 11px 14px; + border: 1.5px solid #e2e8f0; + border-radius: 10px; + font-size: 14px; + color: #1e293b; + outline: none; + transition: border-color 0.2s, box-shadow 0.2s; + font-family: inherit; +} +.kadm-input:focus { border-color: #16a34a; box-shadow: 0 0 0 3px rgba(22,163,74,0.1); } +.kadm-input-wrap { position: relative; } +.kadm-input-wrap .kadm-input { padding-right: 44px; } +.kadm-eye-btn { + position: absolute; right: 12px; top: 50%; transform: translateY(-50%); + background: none; border: none; color: #94a3b8; cursor: pointer; + font-size: 14px; +} +.kadm-error { color: #ef4444; font-size: 12px; margin-top: 5px; } + +/* BUTTONS */ +.kadm-btn-primary { + padding: 12px 24px; + background: linear-gradient(135deg, #16a34a, #15803d); + color: white; + border: none; + border-radius: 12px; + font-size: 14px; + font-weight: 700; + cursor: pointer; + display: flex; align-items: center; gap: 8px; + box-shadow: 0 4px 12px rgba(22,163,74,0.3); + transition: all 0.2s; + font-family: inherit; +} +.kadm-btn-primary:hover { transform: translateY(-1px); box-shadow: 0 6px 16px rgba(22,163,74,0.4); } +.kadm-btn-submit { + padding: 10px 22px; + background: linear-gradient(135deg, #16a34a, #15803d); + color: white; border: none; border-radius: 10px; + font-size: 14px; font-weight: 700; cursor: pointer; + transition: all 0.2s; font-family: inherit; +} +.kadm-btn-submit:hover { opacity: 0.9; } +.kadm-btn-cancel { + padding: 10px 22px; + background: #f1f5f9; + color: #64748b; border: none; border-radius: 10px; + font-size: 14px; font-weight: 600; cursor: pointer; + transition: all 0.2s; font-family: inherit; +} +.kadm-btn-cancel:hover { background: #e2e8f0; } + +/* DETAIL MODAL */ +.kadm-detail-av { + width: 72px; height: 72px; + border-radius: 18px; + display: flex; align-items: center; justify-content: center; + font-weight: 800; font-size: 30px; + background: linear-gradient(135deg, #dcfce7, #bbf7d0); + color: #16a34a; + margin: 0 auto 14px; +} +.kadm-detail-info { + background: #f8fafc; + border-radius: 14px; + border: 1px solid #f1f5f9; + padding: 18px; + display: flex; flex-direction: column; gap: 14px; +} +.kadm-detail-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } +.kadm-detail-label { font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; } +.kadm-detail-val { font-size: 14px; font-weight: 600; color: #1e293b; } +.kadm-divider { height: 1px; background: #f1f5f9; } diff --git a/samooapk/public/CSS/laporan.css b/samooapk/public/CSS/laporan.css new file mode 100644 index 0000000..6fc7191 --- /dev/null +++ b/samooapk/public/CSS/laporan.css @@ -0,0 +1,434 @@ +/* ============================================================ + laporan.css — SIPDAM Premium Dashboard + Inspired by modern clean UI aesthetics + ============================================================ */ + +:root { + --dash-bg: #f8fafc; + --dash-card-bg: #ffffff; + --dash-primary: #0f172a; + --dash-accent: #10b981; + --dash-danger: #ef4444; + --dash-warning: #f59e0b; + --dash-info: #3b82f6; + --dash-border: #e2e8f0; + --dash-text: #1e293b; + --dash-text-m: #64748b; + --dash-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1); + --dash-radius: 12px; +} + +.db-wrap { + background-color: var(--dash-bg); + min-height: 100vh; + font-family: 'Inter', system-ui, -apple-system, sans-serif; + color: var(--dash-text); +} + +/* Robust Grid System */ +.row { + display: flex !important; + flex-wrap: wrap !important; + margin-right: -12px !important; + margin-left: -12px !important; +} + +.col-xl-3, .col-lg-3, .col-md-6, .col-12, .col-lg-6, .col-xl-6 { + position: relative; + width: 100%; + padding-right: 12px; + padding-left: 12px; +} + +@media (min-width: 1200px) { + .col-xl-3 { flex: 0 0 25% !important; max-width: 25% !important; } + .col-xl-6 { flex: 0 0 50% !important; max-width: 50% !important; } +} + +@media (min-width: 992px) { + .col-lg-3 { flex: 0 0 25% !important; max-width: 25% !important; } + .col-lg-6 { flex: 0 0 50% !important; max-width: 50% !important; } +} + +@media (min-width: 768px) { + .col-md-6 { flex: 0 0 50% !important; max-width: 50% !important; } +} + +/* Robust Flex Utilities */ +.d-flex { display: flex !important; } +.justify-content-between { justify-content: space-between !important; } +.align-items-center { align-items: center !important; } +.flex-wrap { flex-wrap: wrap !important; } +.gap-2 { gap: 0.5rem !important; } +.gap-3 { gap: 1rem !important; } + +/* Tab Navigation Style */ +.report-nav { + display: flex; + gap: 12px; + margin-bottom: 24px; + flex-wrap: wrap; +} + +.nav-item-custom { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 20px; + background: white; + border: 1px solid var(--dash-border); + border-radius: 10px; + color: var(--dash-text); + font-weight: 600; + text-decoration: none; + transition: all 0.2s ease; + box-shadow: var(--dash-shadow); +} + +.nav-item-custom:hover { + background: #f1f5f9; + color: var(--dash-primary); +} + +.nav-item-custom.active { + background: #f1f5f9; + border-color: var(--dash-primary); + color: var(--dash-primary); +} + +.nav-item-custom i { + font-size: 1.1rem; + color: var(--dash-text-m); +} + +/* Header Section */ +.dash-header { + margin-bottom: 30px; +} + +.dash-title { + font-size: 1.75rem; + font-weight: 800; + color: var(--dash-primary); + margin-bottom: 4px; +} + +.dash-subtitle { + color: var(--dash-text-m); + font-size: 0.95rem; +} + +/* Clean Summary Cards */ +.sum-card { + background: white; + border: 1px solid var(--dash-border); + border-radius: var(--dash-radius); + padding: 24px; + height: 100%; + transition: transform 0.2s ease; +} + +.sum-card:hover { + transform: translateY(-2px); + box-shadow: 0 10px 15px -3px rgba(0,0,0,0.05); +} + +.sum-label { + font-size: 0.875rem; + font-weight: 600; + color: var(--dash-text-m); + margin-bottom: 12px; + display: block; +} + +.sum-value { + font-size: 2rem; + font-weight: 800; + color: var(--dash-primary); + margin-bottom: 4px; + display: block; +} + +.sum-value.success { color: var(--dash-accent); } +.sum-value.danger { color: var(--dash-danger); } + +.sum-subtext { + font-size: 0.875rem; + color: var(--dash-text-m); +} + +/* Filter Bar */ +.filter-bar { + background: white; + border: 1px solid var(--dash-border); + border-radius: var(--dash-radius); + padding: 16px; + margin-bottom: 24px; + display: flex; + align-items: center; + gap: 16px; +} + +.search-input-group { + position: relative; + flex-grow: 1; +} + +.search-input-group i { + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + color: var(--dash-text-m); +} + +.search-input-group input { + padding-left: 40px; + border-radius: 8px; + border: 1px solid var(--dash-border); + width: 100%; + padding-top: 10px; + padding-bottom: 10px; +} + +/* Modern Table */ +.modern-table-card { + background: white; + border: 1px solid var(--dash-border); + border-radius: var(--dash-radius); + overflow: hidden; + box-shadow: var(--dash-shadow); +} + +.modern-table { + width: 100%; + border-collapse: collapse; +} + +.modern-table th { + background: #f8fafc; + padding: 16px 20px; + text-align: left; + font-size: 0.85rem; + font-weight: 700; + text-transform: uppercase; + color: var(--dash-text-m); + letter-spacing: 0.05em; + border-bottom: 1px solid var(--dash-border); +} + +.modern-table td { + padding: 16px 20px; + border-bottom: 1px solid #f1f5f9; + vertical-align: middle; +} + +.modern-table tr:last-child td { + border-bottom: none; +} + +/* Avatar Circle */ +.avatar-circle { + width: 40px; + height: 40px; + border-radius: 50%; + background: #f1f5f9; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + color: var(--dash-text-m); + font-size: 0.9rem; + border: 2px solid white; + box-shadow: 0 0 0 1px var(--dash-border); +} + +.avatar-info { + display: flex; + flex-direction: column; +} + +.avatar-name { + font-weight: 700; + color: var(--dash-primary); + font-size: 0.95rem; +} + +.avatar-role { + font-size: 0.8rem; + color: var(--dash-text-m); +} + +/* Pill Badges */ +.badge-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + border-radius: 9999px; + font-size: 0.8rem; + font-weight: 700; +} + +.badge-pill::before { + content: ''; + width: 6px; + height: 6px; + border-radius: 50%; +} + +.badge-success { + background: #ecfdf5; + color: #065f46; +} +.badge-success::before { background: #10b981; } + +.badge-pending { + background: #fef3c7; + color: #92400e; +} +.badge-pending::before { background: #f59e0b; } + +.badge-danger { + background: #fef2f2; + color: #991b1b; +} +.badge-danger::before { background: #ef4444; } + +.badge-info { + background: #eff6ff; + color: #1e40af; +} +.badge-info::before { background: #3b82f6; } + +/* Buttons */ +.btn-ekspor { + background: white; + border: 1px solid var(--dash-border); + padding: 8px 16px; + border-radius: 8px; + font-weight: 600; + display: flex; + align-items: center; + gap: 8px; + color: var(--dash-text); +} + +.btn-ekspor:hover { + background: #f1f5f9; +} + +/* Month Select */ +.month-select { + border: 1px solid var(--dash-border); + border-radius: 8px; + padding: 8px 16px; + font-weight: 600; + background: white; +} + +@media (max-width: 768px) { + .report-nav { + flex-direction: column; + } + .filter-bar { + flex-direction: column; + align-items: stretch; + } +} + +/* --- Detail Modal Styles --- */ +.abs-overlay { + position: fixed; + top: 0; left: 0; width: 100%; height: 100%; + background: rgba(15, 23, 42, 0.6); + backdrop-filter: blur(4px); + display: none; + align-items: center; + justify-content: center; + z-index: 9999; + padding: 20px; +} +.abs-overlay.show { display: flex; } + +.abs-modal { + background: #fff; + width: 100%; + max-width: 550px; + border-radius: 20px; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + overflow: hidden; + animation: modalSlideUp 0.3s ease-out; +} +.abs-modal-lg { max-width: 800px; } + +@keyframes modalSlideUp { + from { transform: translateY(30px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +.abs-modal-header { + padding: 20px 24px; + background: #fff; + border-bottom: 1px solid #f1f5f9; + display: flex; + align-items: center; + justify-content: space-between; +} +.abs-modal-title { + font-size: 18px; + font-weight: 700; + color: #1e293b; + display: flex; + align-items: center; + gap: 10px; +} +.abs-close-btn { + background: #f1f5f9; + border: none; + width: 32px; + height: 32px; + border-radius: 8px; + color: #64748b; + cursor: pointer; + transition: all 0.2s; +} +.abs-close-btn:hover { background: #e2e8f0; color: #0f172a; } + +.abs-modal-footer { + padding: 16px 24px; + background: #f8fafc; + border-top: 1px solid #f1f5f9; + display: flex; + justify-content: flex-end; +} + +/* --- Preview Foto Styles --- */ +.abs-preview-overlay { + position: fixed; + top: 0; left: 0; width: 100%; height: 100%; + background: rgba(0,0,0,0.9); + display: none; + align-items: center; + justify-content: center; + z-index: 10000; + cursor: zoom-out; +} +.abs-preview-overlay.show { display: flex; } +.abs-preview-overlay img { + max-width: 90%; + max-height: 90%; + border-radius: 8px; + box-shadow: 0 0 40px rgba(0,0,0,0.5); +} +.abs-preview-x { + position: absolute; + top: 20px; right: 20px; + background: rgba(255,255,255,0.1); + color: #fff; + border: none; + width: 44px; height: 44px; + border-radius: 50%; + font-size: 20px; + cursor: pointer; +} diff --git a/samooapk/public/CSS/penggajian.css b/samooapk/public/CSS/penggajian.css new file mode 100644 index 0000000..d8403e8 --- /dev/null +++ b/samooapk/public/CSS/penggajian.css @@ -0,0 +1,506 @@ +:root { + --bg: #f8fafc; + --card: #ffffff; + --primary: #10b981; /* Emerald 500 */ + --primary-dark: #059669; + --primary-light: rgba(16, 185, 129, 0.1); + --secondary: #0ea5e9; /* Sky 500 */ + --accent: #f59e0b; /* Amber 500 */ + --danger: #ef4444; /* Rose 500 */ + --t1: #0f172a; /* Slate 900 */ + --t2: #334155; /* Slate 700 */ + --t3: #64748b; /* Slate 500 */ + --line: #e2e8f0; /* Slate 200 */ + --line-light: #f1f5f9; /* Slate 100 */ + --shadow: 0 4px 20px -2px rgba(0, 0, 0, 0.05), 0 2px 10px -2px rgba(0, 0, 0, 0.03); + --shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + --mono: 'JetBrains Mono', 'Fira Code', monospace; +} + +.pg-wrap { + padding: 30px; + max-width: 1400px; + margin: 0 auto; + animation: fadeIn 0.5s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +/* ── FORM CARD ── */ +.pg-form-card { + background: var(--card); + border: 1px solid var(--line); + border-radius: 20px; + padding: 24px; + margin-bottom: 24px; + box-shadow: var(--shadow); + position: relative; + overflow: hidden; +} + +.pg-form-card::before { + content: ''; + position: absolute; + top: 0; left: 0; width: 4px; height: 100%; + background: var(--primary); +} + +.pg-form-title { + font-size: 14px; + font-weight: 800; + color: var(--t1); + margin-bottom: 20px; + display: flex; + align-items: center; + gap: 10px; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.pg-form-row { + display: flex; + flex-wrap: wrap; + gap: 16px; + align-items: flex-end; +} + +.pg-form-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.pg-form-group label { + font-size: 11px; + font-weight: 700; + color: var(--t3); + text-transform: uppercase; + letter-spacing: 0.02em; +} + +.pg-select, .pg-date-input { + background: #fdfdfd; + border: 1.5px solid var(--line); + border-radius: 12px; + color: var(--t1); + font-size: 14px; + padding: 10px 14px; + height: 44px; + outline: none; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +.pg-select:focus, .pg-date-input:focus { + border-color: var(--primary); + box-shadow: 0 0 0 4px var(--primary-light); + background: #fff; +} + +.pg-btn-calc { + height: 44px; + padding: 0 24px; + background: linear-gradient(135deg, var(--primary), var(--primary-dark)); + color: #fff; + border: none; + border-radius: 12px; + font-size: 14px; + font-weight: 700; + cursor: pointer; + display: flex; + align-items: center; + gap: 10px; + box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2); + transition: all 0.2s; +} + +.pg-btn-calc:hover { + transform: translateY(-2px); + box-shadow: 0 6px 15px rgba(16, 185, 129, 0.3); +} + +.pg-btn-reset { + height: 44px; + padding: 0 20px; + background: #fff; + color: var(--t2); + border: 1.5px solid var(--line); + border-radius: 12px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; +} + +.pg-btn-reset:hover { + background: var(--line-light); + border-color: var(--t3); +} + +/* ── SUMMARY GRID ── */ +.pg-summary { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 20px; + margin-bottom: 24px; +} + +.pg-summary-item { + background: var(--card); + border: 1px solid var(--line); + border-radius: 20px; + padding: 20px; + display: flex; + align-items: center; + gap: 16px; + box-shadow: var(--shadow); + transition: transform 0.2s; +} + +.pg-summary-item:hover { + transform: translateY(-4px); +} + +.pg-summary-icon { + width: 52px; + height: 52px; + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + flex-shrink: 0; +} + +.pg-summary-icon.green { background: #ecfdf5; color: #10b981; } +.pg-summary-icon.cyan { background: #f0f9ff; color: #0ea5e9; } +.pg-summary-icon.amber { background: #fffbeb; color: #f59e0b; } +.pg-summary-icon.violet { background: #f5f3ff; color: #8b5cf6; } + +.pg-summary-val { + font-size: 20px; + font-weight: 800; + color: var(--t1); + letter-spacing: -0.02em; +} + +.pg-summary-lbl { + font-size: 12px; + font-weight: 600; + color: var(--t3); + margin-top: 2px; +} + +/* ── TABLE CARD ── */ +.pg-table-card { + background: var(--card); + border: 1px solid var(--line); + border-radius: 20px; + box-shadow: var(--shadow); + overflow: hidden; +} + +.pg-table-head { + padding: 20px 24px; + display: flex; + align-items: center; + justify-content: space-between; + background: #fff; + border-bottom: 1px solid var(--line); +} + +.pg-table-head-title { + font-size: 16px; + font-weight: 800; + color: var(--t1); + display: flex; + align-items: center; + gap: 12px; +} + +.pg-btn-pay-all { + padding: 10px 20px; + background: var(--primary-light); + color: var(--primary-dark); + border: 1.5px solid transparent; + border-radius: 10px; + font-size: 13px; + font-weight: 700; + cursor: pointer; + transition: all 0.2s; +} + +.pg-btn-pay-all:hover { + background: var(--primary); + color: #fff; +} + +.pg-table { + width: 100%; + border-collapse: collapse; +} + +.pg-table th { + background: #f8fafc; + padding: 14px 20px; + text-align: left; + font-size: 11px; + font-weight: 800; + text-transform: uppercase; + color: var(--t3); + letter-spacing: 0.05em; + border-bottom: 1px solid var(--line); +} + +.pg-table td { + padding: 16px 20px; + font-size: 14px; + color: var(--t1); + border-bottom: 1px solid var(--line-light); + vertical-align: middle; +} + +.pg-table tbody tr:hover { + background: #f9fafb; +} + +.pg-av { + width: 38px; + height: 38px; + border-radius: 12px; + background: linear-gradient(135deg, #10b981, #3b82f6); + color: #fff; + font-weight: 800; + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; +} + +.pg-name { font-weight: 700; color: var(--t1); margin-bottom: 2px; } +.pg-sub { font-size: 12px; color: var(--t3); } + +.pg-money-pos { color: var(--primary-dark); font-weight: 800; font-family: var(--mono); } +.pg-money-neg { color: var(--danger); font-weight: 800; font-family: var(--mono); } +.pg-money-neu { color: var(--t2); font-weight: 600; font-family: var(--mono); } + +.pg-badge { + padding: 6px 12px; + border-radius: 10px; + font-size: 11px; + font-weight: 700; + display: inline-flex; + align-items: center; + gap: 6px; +} + +.pg-badge-ok { background: #dcfce7; color: #15803d; } +.pg-badge-warn { background: #fef3c7; color: #b45309; } + +.pg-action-wrap { + position: relative; + display: flex; + justify-content: center; +} + +.pg-action-btn { + width: 34px; + height: 34px; + border-radius: 10px; + border: 1.5px solid var(--line); + background: #fff; + color: var(--t2); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; +} + +.pg-action-btn:hover { + border-color: var(--primary); + color: var(--primary); + background: var(--primary-light); +} + +.pg-dropdown { + position: absolute; + right: 0; top: calc(100% + 8px); + background: #fff; + border: 1px solid var(--line); + border-radius: 14px; + min-width: 180px; + box-shadow: var(--shadow-lg); + z-index: 100; + display: none; + overflow: hidden; +} + +.pg-dropdown.show { display: block; animation: scaleUp 0.15s ease-out; } + +@keyframes scaleUp { + from { opacity: 0; transform: scale(0.95); } + to { opacity: 1; transform: scale(1); } +} + +.pg-dropdown-item { + padding: 12px 16px; + font-size: 13px; + font-weight: 600; + color: var(--t2); + cursor: pointer; + display: flex; + align-items: center; + gap: 10px; + transition: all 0.15s; +} + +.pg-dropdown-item:hover { + background: #f8fafc; + color: var(--primary-dark); +} + +/* ── MODAL ── */ +.pg-modal-overlay { + position: fixed; inset: 0; + background: rgba(15, 23, 42, 0.6); + backdrop-filter: blur(4px); + z-index: 1000; + display: none; + align-items: center; + justify-content: center; + padding: 20px; +} + +.pg-modal-overlay.show { display: flex; } + +.pg-modal-box { + background: #fff; + border-radius: 24px; + width: 100%; + max-width: 700px; + max-height: 90vh; + box-shadow: var(--shadow-lg); + display: flex; + flex-direction: column; + animation: modalSlide 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +@keyframes modalSlide { + from { opacity: 0; transform: translateY(30px) scale(0.95); } + to { opacity: 1; transform: translateY(0) scale(1); } +} + +.pg-modal-hd { + padding: 24px; + border-bottom: 1px solid var(--line-light); + display: flex; + align-items: center; + justify-content: space-between; +} + +.pg-modal-hd-title { font-size: 18px; font-weight: 800; color: var(--t1); display: flex; align-items: center; gap: 12px; } + +.pg-modal-body { + padding: 24px; + overflow-y: auto; +} + +.pg-modal-footer { + padding: 20px 24px; + border-top: 1px solid var(--line-light); + display: flex; + justify-content: flex-end; + gap: 12px; +} + +.pg-btn-print { + background: var(--t1); + color: #fff; + border: none; + padding: 10px 24px; + border-radius: 12px; + font-weight: 700; + font-size: 14px; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; +} + +.pg-alert { + position: fixed; + top: 24px; left: 50%; + transform: translateX(-50%); + padding: 14px 24px; + border-radius: 14px; + z-index: 9999; + box-shadow: var(--shadow-lg); + display: flex; + align-items: center; + gap: 12px; + font-weight: 700; + font-size: 14px; +} + +.pg-alert-ok { background: #10b981; color: #fff; } +.pg-alert-err { background: #ef4444; color: #fff; } + +/* ── STATES ── */ +.pg-loading { + display: none; + text-align: center; + padding: 60px 20px; + background: #fff; +} + +.pg-empty { + text-align: center; + padding: 80px 20px; + background: #fff; +} + +.pg-empty-icon { + font-size: 54px; + margin-bottom: 20px; + filter: drop-shadow(0 10px 15px rgba(0,0,0,0.1)); +} + +.pg-empty-title { + font-size: 18px; + font-weight: 800; + color: var(--t1); + margin-bottom: 8px; +} + +.pg-empty-sub { + font-size: 14px; + color: var(--t3); + max-width: 400px; + margin: 0 auto; +} + +.pg-spinner { + width: 40px; + height: 40px; + border: 4px solid var(--line-light); + border-top-color: var(--primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; + margin: 0 auto 16px; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +@media (max-width: 992px) { + .pg-summary { grid-template-columns: repeat(2, 1fr); } +} + +@media (max-width: 640px) { + .pg-summary { grid-template-columns: 1fr; } + .pg-wrap { padding: 16px; } +} diff --git a/samooapk/public/CSS/profile.css b/samooapk/public/CSS/profile.css new file mode 100644 index 0000000..cb0c565 --- /dev/null +++ b/samooapk/public/CSS/profile.css @@ -0,0 +1,244 @@ +/* ============================================================ + profile.css — SIPDAM Profile Edit Page + Theme: Light Green + ============================================================ */ + +:root { + --prf-green: #1a7a4a; + --prf-green-m: #2d9e63; + --prf-green-l: #e8f7ee; + --prf-green-l2: #d0eedd; + --prf-border: #d1e8da; + --prf-t1: #0f172a; + --prf-t2: #334155; + --prf-t3: #64748b; + --prf-t4: #94a3b8; + --prf-bg: #f0f7f3; + --prf-white: #ffffff; + --prf-font: 'Figtree', 'Segoe UI', sans-serif; + --prf-rose: #dc2626; + --prf-rose-l: #fff1f2; +} + +.prf-wrap { padding: 28px 0; background: var(--prf-bg); min-height: 100vh; } +.prf-inner { max-width: 1200px; margin: 0 auto; padding: 0 24px; } + +/* ── Header ── */ +.prf-icon-wrap { + width: 54px; height: 54px; border-radius: 12px; + background: linear-gradient(135deg, var(--prf-green), var(--prf-green-m)); + display: flex; align-items: center; justify-content: center; + color: #fff; font-size: 22px; + box-shadow: 0 4px 14px rgba(26,122,74,0.25); +} +.prf-page-title { + font-family: var(--prf-font); font-size: 24px; + font-weight: 700; color: var(--prf-t1); line-height: 1; +} +.prf-page-sub { font-size: 12px; color: var(--prf-t3); margin-top: 5px; } + +/* ── Grid ── */ +.prf-grid { + display: grid; + grid-template-columns: 350px 1fr; + gap: 24px; + margin-top: 30px; +} + +@media (max-width: 900px) { + .prf-grid { + grid-template-columns: 1fr; + } +} + +/* ── Card ── */ +.prf-card { + background: var(--prf-white); + border: 1px solid var(--prf-border); + border-radius: 16px; + padding: 24px; + box-shadow: 0 1px 4px rgba(0,0,0,0.06); +} + +.prf-avatar-big { + width: 90px; + height: 90px; + border-radius: 50%; + background: linear-gradient(135deg, var(--prf-green), var(--prf-green-m)); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 32px; + font-weight: 800; + margin: 0 auto 16px; + border: 3px solid #fff; + box-shadow: 0 4px 14px rgba(26,122,74,0.2); +} + +.prf-user-name { + font-size: 18px; + font-weight: 700; + color: var(--prf-t1); + text-align: center; +} + +.prf-user-email { + font-size: 13px; + color: var(--prf-t3); + text-align: center; + margin-top: 4px; + margin-bottom: 24px; +} + +.prf-info-list { + border-top: 1px solid var(--prf-border); + padding-top: 16px; + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 24px; +} + +.prf-info-item { + display: flex; + justify-content: space-between; + font-size: 12.5px; +} + +.prf-info-lbl { + color: var(--prf-t3); + font-weight: 500; +} + +.prf-info-val { + color: var(--prf-t1); + font-weight: 600; +} + +/* ── Form Fields ── */ +.prf-card-title { + font-size: 16px; + font-weight: 700; + color: var(--prf-t1); + margin-bottom: 6px; + display: flex; + align-items: center; + gap: 10px; +} + +.prf-card-title i { + color: var(--prf-green); +} + +.prf-card-sub { + font-size: 12px; + color: var(--prf-t3); + margin-bottom: 24px; +} + +.prf-form-fields { + display: flex; + flex-direction: column; + gap: 18px; +} + +.prf-form-field { + display: flex; + flex-direction: column; + gap: 7px; +} + +.prf-form-label { + font-size: 11px; + font-weight: 700; + letter-spacing: .06em; + text-transform: uppercase; + color: var(--prf-t2); +} + +.prf-input { + background: var(--prf-bg); + border: 1px solid var(--prf-border); + color: var(--prf-t1); + border-radius: 10px; + padding: 11px 14px; + font-family: var(--prf-font); + font-size: 13px; + outline: none; + transition: border-color .2s, box-shadow .2s; + width: 100%; +} + +.prf-input:focus { + border-color: var(--prf-green); + box-shadow: 0 0 0 3px rgba(26,122,74,0.08); +} + +.prf-err { + font-size: 11px; + color: var(--prf-rose); + margin-top: 2px; +} + +/* ── Buttons ── */ +.prf-btn-primary { + background: linear-gradient(135deg, var(--prf-green), var(--prf-green-m)); + color: #fff; + font-weight: 700; + font-size: 13px; + padding: 11px 24px; + border-radius: 10px; + border: none; + cursor: pointer; + transition: all .2s; + box-shadow: 0 4px 14px rgba(26,122,74,0.25); + align-self: flex-start; +} + +.prf-btn-primary:hover { + transform: translateY(-2px); + opacity: .95; +} + +.prf-btn-logout-full { + width: 100%; + padding: 12px; + border-radius: 10px; + background: var(--prf-rose); + border: 1px solid var(--prf-rose); + color: #ffffff; + font-size: 13px; + font-weight: 700; + font-family: var(--prf-font); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: all 0.2s; + box-shadow: 0 4px 14px rgba(220,38,38,0.2); +} + +.prf-btn-logout-full:hover { + background: #b91c1c; + border-color: #b91c1c; + transform: translateY(-2px); + box-shadow: 0 6px 18px rgba(220,38,38,0.3); +} + +.prf-alert { + padding: 12px 16px; + border-radius: 10px; + font-size: 13px; + margin-bottom: 20px; + display: flex; + align-items: center; + gap: 10px; +} + +.prf-alert-ok { + background: var(--prf-green-l); + border: 1px solid var(--prf-green-l2); + color: var(--prf-green); +} diff --git a/samooapk/public/CSS/progreskerja.css b/samooapk/public/CSS/progreskerja.css new file mode 100644 index 0000000..7db56b7 --- /dev/null +++ b/samooapk/public/CSS/progreskerja.css @@ -0,0 +1,333 @@ +/* MODERN MONITORING DASHBOARD STYLES */ +:root { + --primary: #4f46e5; + --primary-light: #818cf8; + --success: #10b981; + --warning: #f59e0b; + --danger: #ef4444; + --info: #0ea5e9; + --slate-50: #f8fafc; + --slate-100: #f1f5f9; + --slate-200: #e2e8f0; + --slate-300: #cbd5e1; + --slate-400: #94a3b8; + --slate-500: #64748b; + --slate-600: #475569; + --slate-700: #334155; + --slate-800: #1e293b; + --slate-900: #0f172a; + --card-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --card-shadow-hover: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); +} + +.monitoring-header { + background: white; + padding: 1.25rem 1.75rem; + border-radius: 16px; + margin-bottom: 1.5rem; + border: 1px solid var(--slate-200); + box-shadow: var(--card-shadow); +} + +.monitoring-title { + font-weight: 800; + font-size: 1.5rem; + color: var(--slate-800); + letter-spacing: -0.5px; + margin-bottom: 0.25rem; +} + +/* STATISTICS CARDS */ +.stats-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; +} + +.stat-premium-card { + background: white; + padding: 1rem 1.25rem; + border-radius: 16px; + border: 1px solid var(--slate-200); + display: flex; + align-items: center; + gap: 1rem; + transition: all 0.3s ease; + box-shadow: var(--card-shadow); + position: relative; + overflow: hidden; +} + +.stat-premium-card:hover { + transform: translateY(-3px); + box-shadow: var(--card-shadow-hover); +} + +.stat-icon-wrapper { + width: 44px; + height: 44px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + flex-shrink: 0; +} + +.stat-info h3 { + font-size: 1.25rem; + font-weight: 800; + color: var(--slate-900); + margin: 0; + line-height: 1.2; +} + +.stat-info p { + color: var(--slate-500); + font-size: 0.7rem; + font-weight: 600; + margin: 0; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* CARD VARIANTS */ +.card-blue { border-left: 5px solid var(--primary); } +.card-blue .stat-icon-wrapper { background: #eef2ff; color: var(--primary); } + +.card-amber { border-left: 5px solid var(--warning); } +.card-amber .stat-icon-wrapper { background: #fffbeb; color: var(--warning); } + +.card-emerald { border-left: 5px solid var(--success); } +.card-emerald .stat-icon-wrapper { background: #ecfdf5; color: var(--success); } + +.card-slate { border-left: 5px solid var(--slate-500); } +.card-slate .stat-icon-wrapper { background: #f8fafc; color: var(--slate-500); } + +/* FILTER SECTION */ +.premium-filter { + background: white; + padding: 1.25rem 1.5rem; + border-radius: 16px; + margin-bottom: 2rem; + box-shadow: var(--card-shadow); +} + +.filter-label { + font-size: 0.75rem; + font-weight: 700; + color: var(--slate-500); + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 0.5rem; + display: block; +} + +.form-select-premium, .form-control-premium { + border: 1.5px solid var(--slate-200); + border-radius: 12px; + padding: 0.7rem 1rem; + font-size: 0.9rem; + font-weight: 500; + transition: all 0.2s; + background-color: var(--slate-50); +} + +.form-select-premium:focus, .form-control-premium:focus { + border-color: var(--primary); + box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.1); + background-color: white; +} + +/* PROGRESS GRID */ +.progress-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.25rem; +} + +.premium-progress-card { + background: white; + border-radius: 20px; + border: 1px solid var(--slate-200); + overflow: hidden; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + height: 100%; + display: flex; + flex-direction: column; +} + +.premium-progress-card:hover { + transform: translateY(-5px); + box-shadow: var(--card-shadow-hover); +} + +.card-image-wrapper { + height: 160px; + position: relative; + overflow: hidden; + background: var(--slate-100); +} + +.card-image-wrapper img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.5s ease; +} + +.premium-progress-card:hover .card-image-wrapper img { + transform: scale(1.1); +} + +.status-floating-badge { + position: absolute; + top: 1rem; + right: 1rem; + padding: 0.5rem 1rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 800; + backdrop-filter: blur(8px); + background: rgba(255, 255, 255, 0.9); + box-shadow: 0 4px 6px rgba(0,0,0,0.05); + display: flex; + align-items: center; + gap: 0.5rem; + z-index: 2; +} + +.status-belum_mulai { color: var(--slate-600); border: 1px solid var(--slate-200); } +.status-dalam_proses { color: var(--warning); border: 1px solid rgba(245, 158, 11, 0.3); background: rgba(255, 251, 235, 0.9); } +.status-selesai { color: var(--success); border: 1px solid rgba(16, 185, 129, 0.3); background: rgba(236, 253, 245, 0.9); } + +.card-main-content { + padding: 1.25rem; + flex-grow: 1; +} + +.job-type-label { + font-size: 0.9rem; + font-weight: 800; + color: var(--slate-800); + margin-bottom: 0.75rem; + line-height: 1.4; + display: block; +} + +.tech-team-section { + background: var(--slate-50); + padding: 0.5rem 0.75rem; + border-radius: 12px; + margin-bottom: 1rem; + border: 1px dashed var(--slate-300); +} + +.tech-name { + font-size: 0.85rem; + font-weight: 700; + color: var(--slate-700); +} + +.team-badge { + display: inline-block; + background: var(--primary-light); + color: white; + font-size: 10px; + padding: 2px 6px; + border-radius: 4px; + margin-left: 5px; + text-transform: uppercase; +} + +/* PROGRESS BAR PREMIUM */ +.premium-progress-container { + margin-top: 1.5rem; +} + +.progress-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.progress-label { + font-size: 0.75rem; + font-weight: 700; + color: var(--slate-500); +} + +.progress-value { + font-size: 0.875rem; + font-weight: 800; + color: var(--primary); +} + +.progress-track { + height: 10px; + background: var(--slate-100); + border-radius: 99px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--primary) 0%, var(--primary-light) 100%); + border-radius: 99px; + transition: width 1s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* CARD FOOTER */ +.card-premium-footer { + padding: 1rem 1.5rem; + background: var(--slate-50); + border-top: 1px solid var(--slate-200); + display: flex; + justify-content: space-between; + align-items: center; +} + +.btn-premium-action { + padding: 0.5rem 1.25rem; + border-radius: 10px; + font-weight: 700; + font-size: 0.8rem; + transition: all 0.2s; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.btn-view { + background: white; + color: var(--slate-700); + border: 1px solid var(--slate-200); +} + +.btn-view:hover { + background: var(--slate-100); + color: var(--slate-900); +} + +.btn-edit { + background: var(--warning); + color: white; + border: none; +} + +.btn-edit:hover { + filter: brightness(1.1); + box-shadow: 0 4px 10px rgba(245, 158, 11, 0.2); +} + +/* RESPONSIVE */ +@media (max-width: 768px) { + .monitoring-header { padding: 1.5rem; } + .stats-container { grid-template-columns: 1fr 1fr; } +} + +@media (max-width: 480px) { + .stats-container { grid-template-columns: 1fr; } +} diff --git a/samooapk/public/CSS/sidebar.css b/samooapk/public/CSS/sidebar.css new file mode 100644 index 0000000..cb7c822 --- /dev/null +++ b/samooapk/public/CSS/sidebar.css @@ -0,0 +1,491 @@ +/* ============================================================ + sidebar.css — SIPDAM Sidebar Toggle + ============================================================ */ + +:root { + --sb-green: #1a7a4a; + --sb-green-m: #2d9e63; + --sb-green-l: #e8f7ee; + --sb-green-l2: #d0eedd; + --sb-rose: #dc2626; + --sb-rose-l: #fff1f2; + --sb-border: #d1e8da; + --sb-t1: #0f172a; + --sb-t2: #334155; + --sb-t3: #64748b; + --sb-t4: #94a3b8; + --sb-font: 'Figtree', 'Segoe UI', sans-serif; + --sb-width: 190px; + --sb-collapsed-width: 68px; +} + +/* ── App Wrapper ── */ +.app-wrapper { + display: flex; + min-height: 100vh; +} + +/* ── Main Wrapper ── */ +.main-wrapper { + flex: 1; + margin-left: var(--sb-width); + transition: margin-left 0.3s ease; + min-height: 100vh; + background: #f0f7f3; +} + +.main-wrapper.sb-collapsed { + margin-left: var(--sb-collapsed-width); +} + +/* ── Base Sidebar ── */ +.sb { + position: fixed; + left: 0; + top: 0; + width: var(--sb-width); + height: 100vh; + z-index: 50; + display: flex; + flex-direction: column; + background: #ffffff; + border-right: 1px solid var(--sb-border); + box-shadow: 4px 0 24px rgba(26, 122, 74, 0.08); + transition: width 0.3s ease; + overflow: hidden; + font-family: var(--sb-font); +} + +.sb.sb-collapsed { + width: var(--sb-collapsed-width); +} + +/* ── Logo Header ── */ +.sb-head { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 12px; + border-bottom: 1px solid var(--sb-border); + flex-shrink: 0; + background: linear-gradient(135deg, var(--sb-green) 0%, var(--sb-green-m) 100%); + min-height: 76px; + overflow: hidden; + white-space: nowrap; +} + +.sb-logo { + width: 44px; + height: 44px; + border-radius: 12px; + flex-shrink: 0; + background: rgba(255,255,255,0.2); + border: 2px solid rgba(255,255,255,0.35); + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 18px; +} + +.sb-wordmark { + overflow: hidden; + transition: opacity 0.2s ease, width 0.3s ease; + white-space: nowrap; +} + +.sb.sb-collapsed .sb-wordmark { + opacity: 0; + width: 0; +} + +.sb-wordmark-main { + font-size: 16px; + font-weight: 800; + color: #fff; + letter-spacing: 0.02em; + line-height: 1; +} + +.sb-wordmark-sub { + font-size: 10px; + color: rgba(255,255,255,0.65); + margin-top: 3px; +} + +/* ── Navigation ── */ +.sb-nav { + padding: 14px 8px; + flex: 1; + overflow-y: auto; + overflow-x: hidden; + scrollbar-width: none; + list-style: none; + margin: 0; +} + +.sb-nav::-webkit-scrollbar { display: none; } + +/* Section label */ +.menu-section { + font-size: 10px; + font-weight: 700; + color: var(--sb-t4); + text-transform: uppercase; + letter-spacing: 0.07em; + padding: 12px 8px 4px; + white-space: nowrap; + overflow: hidden; + transition: opacity 0.2s ease; +} + +.sb.sb-collapsed .menu-section { + opacity: 0; + height: 0; + padding: 0; +} + +/* Nav links */ +.sb-nav > li > a, +.sb-nav > li > .menu-parent { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 9px 10px; + border-radius: 10px; + background: none; + border: none; + cursor: pointer; + color: var(--sb-t2); + font-size: 13px; + font-weight: 500; + font-family: var(--sb-font); + text-decoration: none; + transition: background 0.15s, color 0.15s; + text-align: left; + box-sizing: border-box; + white-space: nowrap; + margin-bottom: 2px; +} + +.sb-nav > li > a:hover, +.sb-nav > li > .menu-parent:hover { + background: var(--sb-green-l); + color: var(--sb-green); +} + +.sb-nav > li > a.active, +.sb-nav > li.active > .menu-parent { + background: var(--sb-green-l); + color: var(--sb-green); + font-weight: 700; +} + +/* Icon */ +.sb-nav > li > a > i:first-child, +.sb-nav > li > .menu-parent > i:first-child { + width: 34px; + height: 34px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + font-size: 14px; + transition: background 0.15s; +} + +.sb-nav > li > a.active > i:first-child, +.sb-nav > li > a:hover > i:first-child, +.sb-nav > li.active > .menu-parent > i:first-child, +.sb-nav > li > .menu-parent:hover > i:first-child { + background: var(--sb-green-l2); + color: var(--sb-green); +} + +/* Label span */ +.sb-nav > li > a > span, +.sb-nav > li > .menu-parent > span { + flex: 1; + transition: opacity 0.2s ease; + white-space: nowrap; +} + +.sb.sb-collapsed .sb-nav > li > a > span, +.sb.sb-collapsed .sb-nav > li > .menu-parent > span { + opacity: 0; + width: 0; + overflow: hidden; +} + +/* Toggle chevron */ +.toggle-icon { + font-size: 10px; + color: var(--sb-t4); + margin-left: auto; + transition: transform 0.25s, opacity 0.2s; + flex-shrink: 0; +} + +.menu-toggle.active > .menu-parent > .toggle-icon { + transform: rotate(180deg); +} + +.sb.sb-collapsed .toggle-icon { + opacity: 0; + width: 0; +} + +/* ── Submenu ── */ +.menu-toggle { margin-bottom: 2px; } + +.submenu { + list-style: none; + padding-left: 10px; + margin: 0; + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease; +} + +.menu-toggle.active > .submenu { + max-height: 400px; +} + +.sb.sb-collapsed .submenu { + max-height: 0 !important; +} + +.submenu > li > a { + display: flex; + align-items: center; + gap: 9px; + padding: 7px 10px; + border-radius: 8px; + font-size: 12.5px; + font-weight: 500; + color: var(--sb-t2); + font-family: var(--sb-font); + text-decoration: none; + margin-bottom: 2px; + transition: background 0.15s, color 0.15s; + border-left: 2px solid transparent; + white-space: nowrap; +} + +.submenu > li > a:hover { + background: var(--sb-green-l); + color: var(--sb-green); + border-left-color: var(--sb-green-l2); +} + +.submenu > li > a.active { + background: var(--sb-green-l); + color: var(--sb-green); + font-weight: 700; + border-left-color: var(--sb-green); +} + +.submenu > li > a > i { + width: 16px; + text-align: center; + flex-shrink: 0; +} + +/* ── Toggle Button ── */ +.sb-toggle-btn { + width: 36px; + height: 36px; + border-radius: 8px; + background: var(--sb-green-l); + border: none; + color: var(--sb-green); + font-size: 15px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: background 0.15s; +} + +.sb-toggle-btn:hover { + background: var(--sb-green-l2); +} + +/* ── Topbar ── */ +.main-topbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 24px; + background: #fff; + border-bottom: 1px solid var(--sb-border); + gap: 12px; +} + +.topbar-left { + display: flex; + align-items: center; + gap: 12px; +} + +.topbar-icon { + width: 36px; + height: 36px; + border-radius: 8px; + background: var(--sb-green-l); + color: var(--sb-green); + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; +} + +.topbar-title { + font-size: 14px; + font-weight: 700; + color: var(--sb-t1); +} + +.topbar-sub { + font-size: 12px; + color: var(--sb-t3); +} + +.topbar-right { display: flex; align-items: center; gap: 10px; } + +.clock-pill { + display: flex; + align-items: center; + gap: 7px; + background: var(--sb-green-l); + border: 1px solid var(--sb-border); + padding: 6px 14px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + color: var(--sb-green); +} + +.dot-live { + width: 7px; + height: 7px; + border-radius: 50%; + background: #22c55e; + animation: pulse 1.5s infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.3; } +} + +/* ── Main Content ── */ +.main-content { + padding: 24px; +} + +/* ── User Profile Bottom ── */ +.sb-bottom { + flex-shrink: 0; + border-top: 1px solid var(--sb-border); + background: #f0f7f3; + padding: 12px; + overflow: hidden; +} + +.sb-user { + display: flex; + align-items: center; + gap: 10px; + white-space: nowrap; +} + +.sb-avatar { + width: 38px; + height: 38px; + border-radius: 10px; + flex-shrink: 0; + background: linear-gradient(135deg, var(--sb-green), var(--sb-green-m)); + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 13px; + font-weight: 800; + font-family: var(--sb-font); +} + +.sb-user-info { + overflow: hidden; + transition: opacity 0.2s ease; +} + +.sb.sb-collapsed .sb-user-info { + opacity: 0; + width: 0; +} + +.sb-user-name { + font-size: 12px; + font-weight: 700; + color: var(--sb-t1); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.sb-user-email { + font-size: 10px; + color: var(--sb-t3); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-top: 2px; +} + +.sb-logout-btn { + width: 100%; + margin-top: 10px; + padding: 9px; + border-radius: 10px; + background: var(--sb-rose-l); + border: 1px solid #fca5a5; + color: var(--sb-rose); + font-size: 12px; + font-weight: 700; + font-family: var(--sb-font); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: background 0.2s; + white-space: nowrap; + overflow: hidden; +} + +.sb.sb-collapsed .sb-logout-btn span { + display: none; +} + +.sb-logout-btn:hover { + background: #fecaca; + border-color: var(--sb-rose); +} + +/* ── Responsive ── */ +@media (max-width: 768px) { + .sb { + transform: translateX(-100%); + width: var(--sb-width) !important; + } + .sb.sb-mobile-open { + transform: translateX(0); + } + .main-wrapper { + margin-left: 0 !important; + } +} \ No newline at end of file diff --git a/samooapk/public/CSS/teknisi.css b/samooapk/public/CSS/teknisi.css new file mode 100644 index 0000000..aacb0ae --- /dev/null +++ b/samooapk/public/CSS/teknisi.css @@ -0,0 +1,377 @@ +/* ============================================================ + teknisi.css — SIPDAM Teknisi Page + Theme: Light Green (sama dengan dashboard) + ============================================================ */ + +@import url('https://fonts.googleapis.com/css2?family=Figtree:wght@400;500;600;700;800&display=swap'); + +:root { + --tek-green: #1a7a4a; + --tek-green-m: #2d9e63; + --tek-green-l: #e8f7ee; + --tek-green-l2: #d0eedd; + --tek-teal: #0d9488; + --tek-blue: #2563eb; + --tek-blue-l: #eff6ff; + --tek-violet: #7c3aed; + --tek-violet-l: #f5f3ff; + --tek-amber: #d97706; + --tek-amber-l: #fffbeb; + --tek-rose: #dc2626; + --tek-rose-l: #fff1f2; + --tek-cyan: #0891b2; + --tek-cyan-l: #ecfeff; + --tek-border: #d1e8da; + --tek-t1: #0f172a; + --tek-t2: #334155; + --tek-t3: #64748b; + --tek-t4: #94a3b8; + --tek-bg: #f0f7f3; + --tek-white: #ffffff; + --tek-font: 'Figtree', 'Segoe UI', sans-serif; +} + +/* ── Page header ── */ +.tek-icon-wrap { + width: 44px; height: 44px; border-radius: 12px; + background: linear-gradient(135deg, var(--tek-green), var(--tek-green-m)); + display: flex; align-items: center; justify-content: center; + color: #fff; font-size: 18px; + box-shadow: 0 4px 14px rgba(26,122,74,0.25); +} +.tek-page-title { + font-family: var(--tek-font); font-size: 20px; + font-weight: 700; color: var(--tek-t1); line-height: 1; +} +.tek-page-sub { font-size: 12px; color: var(--tek-t3); margin-top: 3px; } + +.tek-btn-primary { + font-family: var(--tek-font); + background: linear-gradient(135deg, var(--tek-green), var(--tek-green-m)); + color: #fff; font-weight: 700; font-size: 13px; + padding: 10px 20px; border-radius: 10px; border: none; cursor: pointer; + display: flex; align-items: center; gap: 8px; transition: all .2s; + box-shadow: 0 4px 14px rgba(26,122,74,0.25); +} +.tek-btn-primary:hover { transform: translateY(-2px); opacity: .9; } + +/* ── Wrap ── */ +.tek-wrap { padding: 28px 0; background: var(--tek-bg); min-height: 100vh; } +.tek-inner { max-width: 1400px; margin: 0 auto; padding: 0 24px; } + +/* ── Stats ── */ +.tek-stats { + display: grid; grid-template-columns: repeat(3,1fr); + gap: 16px; margin-bottom: 24px; +} +@media(max-width:720px){ .tek-stats { grid-template-columns: 1fr; } } + +.tek-stat { + background: var(--tek-white); + border-radius: 16px; padding: 22px; + position: relative; overflow: hidden; + transition: transform .2s, box-shadow .2s; + box-shadow: 0 1px 4px rgba(0,0,0,0.06); +} +.tek-stat:hover { transform: translateY(-3px); box-shadow: 0 8px 24px rgba(26,122,74,0.12); } +.tek-stat::before { content:''; position:absolute; bottom:0; left:0; right:0; height:3px; border-radius: 0 0 16px 16px; } +.tek-stat-total::before { background: linear-gradient(90deg, var(--tek-green), var(--tek-green-m)); } +.tek-stat-aktif::before { background: linear-gradient(90deg, var(--tek-teal), #06b6d4); } +.tek-stat-nonaktif::before { background: linear-gradient(90deg, var(--tek-rose), #f87171); } + +.tek-stat-label { + font-family: var(--tek-font); font-size: 10px; font-weight: 700; + letter-spacing: .08em; text-transform: uppercase; + color: var(--tek-t3); margin-bottom: 10px; +} +.tek-stat-val { + font-family: var(--tek-font); font-size: 38px; + font-weight: 800; color: var(--tek-t1); line-height: 1; margin-bottom: 6px; +} +.tek-stat-sub { font-size: 11px; color: var(--tek-t3); } +.tek-stat-icon { + position: absolute; right: 18px; top: 18px; + width: 42px; height: 42px; border-radius: 11px; + display: flex; align-items: center; justify-content: center; font-size: 17px; +} +.tek-stat-total .tek-stat-icon { background: var(--tek-green-l); color: var(--tek-green); } +.tek-stat-aktif .tek-stat-icon { background: var(--tek-cyan-l); color: var(--tek-cyan); } +.tek-stat-nonaktif .tek-stat-icon { background: var(--tek-rose-l); color: var(--tek-rose); } + +/* ── Toolbar ── */ +.tek-toolbar { + display: flex; align-items: center; gap: 12px; + flex-wrap: wrap; margin-bottom: 18px; +} +.tek-search-wrap { flex: 1; min-width: 220px; position: relative; } +.tek-search-wrap i { + position: absolute; left: 14px; top: 50%; + transform: translateY(-50%); color: var(--tek-t4); + font-size: 13px; pointer-events: none; +} +.tek-search-input { + width: 100%; background: var(--tek-white); + border: 1px solid var(--tek-border); color: var(--tek-t1); + border-radius: 10px; padding: 11px 14px 11px 38px; + font-family: var(--tek-font); font-size: 13px; + outline: none; transition: border-color .2s; +} +.tek-search-input::placeholder { color: var(--tek-t4); } +.tek-search-input:focus { border-color: var(--tek-green); box-shadow: 0 0 0 3px rgba(26,122,74,0.08); } + +.tek-refresh-btn { + width: 42px; height: 42px; border-radius: 10px; + background: var(--tek-white); border: 1px solid var(--tek-border); + color: var(--tek-t3); display: flex; align-items: center; justify-content: center; + cursor: pointer; transition: all .2s; font-size: 13px; +} +.tek-refresh-btn:hover { + border-color: var(--tek-green); color: var(--tek-green); + background: var(--tek-green-l); +} +.tek-refresh-btn.spinning i { animation: tek-spin .7s linear infinite; } +@keyframes tek-spin { to { transform: rotate(360deg); } } + +/* ── Panel ── */ +.tek-panel { + background: var(--tek-white); border: 1px solid var(--tek-border); + border-radius: 16px; overflow: hidden; + box-shadow: 0 1px 4px rgba(0,0,0,0.06); +} +.tek-panel-head { + padding: 16px 24px; border-bottom: 1px solid var(--tek-border); + display: flex; align-items: center; justify-content: space-between; + background: #f8fdf9; +} +.tek-panel-head-left { + font-family: var(--tek-font); font-size: 13px; + font-weight: 700; color: var(--tek-t1); + display: flex; align-items: center; gap: 8px; +} +.tek-panel-head-right { font-size: 12px; color: var(--tek-t3); } + +/* ── Table ── */ +.tek-table { width: 100%; border-collapse: collapse; } +.tek-table thead tr { background: #f8fdf9; } +.tek-table th { + padding: 13px 18px; text-align: left; + font-family: var(--tek-font); font-size: 10px; font-weight: 700; + letter-spacing: .08em; text-transform: uppercase; color: var(--tek-t3); + border-bottom: 1px solid var(--tek-border); +} +.tek-table th:first-child, .tek-table th:last-child { text-align: center; } +.tek-table td { + padding: 14px 18px; border-bottom: 1px solid #f0f7f3; + vertical-align: middle; color: var(--tek-t2); font-size: 13px; + font-family: var(--tek-font); +} +.tek-table tr:last-child td { border-bottom: none; } +.tek-table tbody tr { transition: background .15s; } +.tek-table tbody tr:hover { background: #f8fdf9; } + +.tek-rownum { + width: 28px; height: 28px; border-radius: 7px; + background: var(--tek-green-l); border: 1px solid var(--tek-green-l2); + color: var(--tek-green); font-size: 11px; font-weight: 700; + display: inline-flex; align-items: center; justify-content: center; margin: auto; +} + +/* Teknisi cell */ +.tek-av-wrap { display: flex; align-items: center; gap: 12px; } +.tek-av { + width: 40px; height: 40px; border-radius: 11px; flex-shrink: 0; position: relative; + background: linear-gradient(135deg, var(--tek-green), var(--tek-green-m)); + color: #fff; font-size: 14px; font-weight: 700; + display: flex; align-items: center; justify-content: center; +} +.tek-av-dot { + position: absolute; bottom: -2px; right: -2px; + width: 11px; height: 11px; border-radius: 50%; + border: 2px solid var(--tek-white); +} +.tek-av-dot.aktif { background: #22c55e; } +.tek-av-dot.nonaktif { background: var(--tek-rose); } +.tek-av-name { font-size: 13px; font-weight: 600; color: var(--tek-t1); font-family: var(--tek-font); } +.tek-av-sub { font-size: 11px; color: var(--tek-t3); margin-top: 2px; } + +/* Info chip */ +.tek-info-chip { + display: inline-flex; align-items: center; gap: 7px; + font-size: 12px; color: var(--tek-t2); font-family: var(--tek-font); +} +.tek-info-chip i { font-size: 11px; } +.tek-info-chip.empty { color: var(--tek-t4); font-style: italic; } + +/* Badge */ +.tek-badge { + display: inline-flex; align-items: center; gap: 5px; + padding: 4px 10px; border-radius: 99px; + font-size: 11px; font-weight: 700; font-family: var(--tek-font); border: 1px solid; +} +.tek-badge-green { background: var(--tek-green-l); color: var(--tek-green); border-color: var(--tek-green-l2); } +.tek-badge-rose { background: var(--tek-rose-l); color: var(--tek-rose); border-color: #fca5a5; } + +/* Action btn */ +.tek-actions { display: flex; align-items: center; justify-content: center; gap: 6px; } +.tek-action-btn { + width: 32px; height: 32px; border-radius: 8px; + border: 1px solid var(--tek-border); background: transparent; + display: flex; align-items: center; justify-content: center; + cursor: pointer; transition: all .2s; font-size: 12px; +} +.tek-action-edit { color: var(--tek-teal); } +.tek-action-edit:hover { background: var(--tek-cyan-l); border-color: var(--tek-teal); } +.tek-action-del { color: var(--tek-rose); } +.tek-action-del:hover { background: var(--tek-rose-l); border-color: var(--tek-rose); } + +/* Empty / loading */ +.tek-center-state { padding: 72px 20px; text-align: center; } +.tek-state-icon { + width: 68px; height: 68px; border-radius: 16px; + background: var(--tek-green-l); border: 1px solid var(--tek-green-l2); + margin: 0 auto 18px; display: flex; align-items: center; justify-content: center; + font-size: 26px; color: var(--tek-green); +} +.tek-state-title { + font-family: var(--tek-font); font-size: 14px; + font-weight: 700; color: var(--tek-t2); margin-bottom: 5px; +} +.tek-state-sub { font-size: 12px; color: var(--tek-t3); } +.tek-spinner-ring { + width: 48px; height: 48px; border-radius: 50%; + border: 3px solid var(--tek-border); border-top-color: var(--tek-green); + animation: tek-spin .7s linear infinite; + margin: 0 auto 18px; +} + +/* Pagination strip */ +.tek-pag { + padding: 14px 24px; border-top: 1px solid var(--tek-border); + background: #f8fdf9; display: flex; align-items: center; gap: 8px; +} +.tek-pag-info { font-size: 12px; color: var(--tek-t3); font-family: var(--tek-font); } +.tek-pag-accent { color: var(--tek-green); font-weight: 700; } + +/* ── Toast ── */ +.tek-toast { + position: fixed; top: 20px; right: 20px; z-index: 999; + padding: 14px 18px; border-radius: 12px; + display: flex; align-items: center; gap: 10px; + font-size: 13px; font-family: var(--tek-font); font-weight: 600; + box-shadow: 0 8px 32px rgba(0,0,0,0.12); + animation: tek-toastIn .3s cubic-bezier(.34,1.56,.64,1); + transition: opacity .4s, transform .4s; +} +@keyframes tek-toastIn { + from { opacity:0; transform: translateX(30px); } + to { opacity:1; transform: none; } +} +.tek-toast-ok { + background: var(--tek-white); border: 1px solid var(--tek-green-l2); + color: var(--tek-green); +} +.tek-toast-err { + background: var(--tek-white); border: 1px solid #fca5a5; + color: var(--tek-rose); +} + +/* ── Modal ── */ +.tek-overlay { + display: none; position: fixed; inset: 0; + background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); + z-index: 50; overflow-y: auto; +} +.tek-overlay.show { + display: flex; align-items: flex-start; + justify-content: center; padding: 40px 16px; +} +.tek-modal { + background: var(--tek-white); border: 1px solid var(--tek-border); + border-radius: 20px; width: 100%; max-width: 760px; + box-shadow: 0 24px 60px rgba(0,0,0,0.15); overflow: hidden; + animation: tek-modalIn .25s cubic-bezier(.34,1.56,.64,1); +} +@keyframes tek-modalIn { + from { opacity:0; transform: scale(.94) translateY(12px); } + to { opacity:1; transform: none; } +} +.tek-modal-stripe { + height: 3px; + background: linear-gradient(90deg, var(--tek-green), var(--tek-green-m), var(--tek-teal)); +} +.tek-modal-inner { padding: 28px 32px 32px; } +.tek-modal-header { + display: flex; align-items: center; justify-content: space-between; + margin-bottom: 26px; padding-bottom: 18px; + border-bottom: 1px solid var(--tek-border); +} +.tek-modal-title { + font-family: var(--tek-font); font-size: 17px; + font-weight: 700; color: var(--tek-t1); + display: flex; align-items: center; gap: 10px; +} +.tek-modal-title i { color: var(--tek-green); } +.tek-close-btn { + width: 32px; height: 32px; border-radius: 8px; + border: 1px solid var(--tek-border); color: var(--tek-t3); + background: transparent; cursor: pointer; + display: flex; align-items: center; justify-content: center; + font-size: 12px; transition: all .2s; +} +.tek-close-btn:hover { border-color: var(--tek-rose); color: var(--tek-rose); } + +/* Form grid */ +.tek-form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; } +@media(max-width:600px) { .tek-form-grid { grid-template-columns: 1fr; } } +.tek-form-full { grid-column: 1/-1; } + +.tek-form-field { display: flex; flex-direction: column; gap: 7px; } +.tek-form-label { + font-size: 11px; font-weight: 700; letter-spacing: .06em; + text-transform: uppercase; color: var(--tek-t2); + display: flex; align-items: center; gap: 6px; +} +.tek-form-label i { font-size: 10px; color: var(--tek-green); } +.tek-form-label .req { color: var(--tek-rose); } +.tek-form-label .opt { color: var(--tek-t4); font-weight: 400; text-transform: none; letter-spacing: 0; font-size: 10px; } +.tek-form-label .hint { color: var(--tek-t4); font-weight: 400; text-transform: none; letter-spacing: 0; font-size: 10px; } + +.tek-input, .tek-select, .tek-textarea { + background: var(--tek-bg); border: 1px solid var(--tek-border); + color: var(--tek-t1); border-radius: 10px; padding: 11px 14px; + font-family: var(--tek-font); font-size: 13px; + outline: none; transition: border-color .2s, box-shadow .2s; width: 100%; +} +.tek-input:focus, .tek-select:focus, .tek-textarea:focus { + border-color: var(--tek-green); + box-shadow: 0 0 0 3px rgba(26,122,74,0.08); +} +.tek-input::placeholder, .tek-textarea::placeholder { color: var(--tek-t4); } +.tek-textarea { resize: none; } +.tek-input.readonly-field { + background: #f1f5f9; color: var(--tek-t4); cursor: not-allowed; +} + +.tek-modal-footer { + display: flex; align-items: center; justify-content: flex-end; + gap: 10px; padding-top: 22px; margin-top: 6px; + border-top: 1px solid var(--tek-border); +} +.tek-btn-cancel { + padding: 10px 20px; border-radius: 10px; font-size: 13px; + font-weight: 600; font-family: var(--tek-font); + background: transparent; border: 1px solid var(--tek-border); + color: var(--tek-t2); cursor: pointer; + display: flex; align-items: center; gap: 7px; transition: all .2s; +} +.tek-btn-cancel:hover { border-color: var(--tek-rose); color: var(--tek-rose); } +.tek-btn-submit { + padding: 10px 24px; border-radius: 10px; font-size: 13px; + font-weight: 700; font-family: var(--tek-font); + background: linear-gradient(135deg, var(--tek-green), var(--tek-green-m)); + color: #fff; border: none; cursor: pointer; + display: flex; align-items: center; gap: 7px; transition: all .2s; + box-shadow: 0 4px 14px rgba(26,122,74,0.25); +} +.tek-btn-submit:hover { opacity: .9; transform: translateY(-1px); } +.tek-btn-submit:disabled { opacity: .5; cursor: not-allowed; transform: none; } \ No newline at end of file diff --git a/samooapk/public/CSS/tugas.css b/samooapk/public/CSS/tugas.css new file mode 100644 index 0000000..c568c70 --- /dev/null +++ b/samooapk/public/CSS/tugas.css @@ -0,0 +1,529 @@ +/* ============================================================ + penugasan.css — SIPDAM Data Penugasan + Theme: Light Green + ============================================================ */ + +@import url('https://fonts.googleapis.com/css2?family=Figtree:wght@400;500;600;700;800&display=swap'); + +:root { + --pug-green: #1a7a4a; + --pug-green-m: #2d9e63; + --pug-green-l: #e8f7ee; + --pug-green-l2: #d0eedd; + --pug-teal: #0d9488; + --pug-cyan: #0891b2; + --pug-cyan-l: #ecfeff; + --pug-amber: #d97706; + --pug-amber-l: #fffbeb; + --pug-rose: #dc2626; + --pug-rose-l: #fff1f2; + --pug-violet: #7c3aed; + --pug-violet-l: #f5f3ff; + --pug-border: #d1e8da; + --pug-t1: #0f172a; + --pug-t2: #334155; + --pug-t3: #64748b; + --pug-t4: #94a3b8; + --pug-bg: #f0f7f3; + --pug-white: #ffffff; + --pug-font: 'Figtree', 'Segoe UI', sans-serif; +} + +/* ── Header ── */ +.pug-icon-wrap { + width: 44px; height: 44px; border-radius: 12px; + background: linear-gradient(135deg, var(--pug-green), var(--pug-green-m)); + display: flex; align-items: center; justify-content: center; + color: #fff; font-size: 18px; + box-shadow: 0 4px 14px rgba(26,122,74,0.25); +} +.pug-page-title { + font-family: var(--pug-font); font-size: 20px; + font-weight: 700; color: var(--pug-t1); line-height: 1; +} +.pug-page-sub { font-size: 12px; color: var(--pug-t3); margin-top: 3px; } + +.pug-btn-primary { + font-family: var(--pug-font); + background: linear-gradient(135deg, var(--pug-green), var(--pug-green-m)); + color: #fff; font-weight: 700; font-size: 13px; + padding: 10px 20px; border-radius: 10px; border: none; + display: flex; align-items: center; gap: 8px; + cursor: pointer; transition: all .2s; + box-shadow: 0 4px 14px rgba(26,122,74,0.25); +} +.pug-btn-primary:hover { transform: translateY(-2px); opacity: .9; } + +/* ── Layout ── */ +.pug-wrap { padding: 28px 0; background: var(--pug-bg); min-height: 100vh; } +.pug-inner { max-width: 1400px; margin: 0 auto; padding: 0 24px; } + +/* ── Alert ── */ +.pug-alert { + padding: 14px 18px; border-radius: 12px; + display: flex; align-items: center; gap: 10px; margin-bottom: 24px; + font-size: 14px; font-family: var(--pug-font); +} +.pug-alert-ok { background: var(--pug-green-l); border: 1px solid var(--pug-green-l2); color: var(--pug-green); } +.pug-alert-err { background: var(--pug-rose-l); border: 1px solid #fca5a5; color: var(--pug-rose); } + +/* ── Stats ── */ +.pug-stats { + display: grid; grid-template-columns: repeat(6, 1fr); + gap: 16px; margin-bottom: 28px; +} +@media(max-width:1100px) { .pug-stats { grid-template-columns: repeat(3, 1fr); } } +@media(max-width:640px) { .pug-stats { grid-template-columns: 1fr 1fr; } } + +.pug-stat { + background: var(--pug-white); border-radius: 16px; padding: 20px; + position: relative; overflow: hidden; + transition: transform .2s, box-shadow .2s; + box-shadow: 0 1px 4px rgba(0,0,0,0.06); + border: 1px solid var(--pug-border); +} +.pug-stat:hover { transform: translateY(-3px); box-shadow: 0 8px 24px rgba(26,122,74,0.12); } +.pug-stat::before { content:''; position:absolute; bottom:0; left:0; right:0; height:3px; border-radius:0 0 16px 16px; } +.pug-stat-total::before { background: linear-gradient(90deg, var(--pug-cyan), #06b6d4); } +.pug-stat-belum::before { background: linear-gradient(90deg, var(--pug-amber), #fbbf24); } +.pug-stat-proses::before { background: linear-gradient(90deg, var(--pug-violet), #a78bfa); } +.pug-stat-selesai::before { background: linear-gradient(90deg, var(--pug-green), var(--pug-green-m)); } +.pug-stat-batal::before { background: linear-gradient(90deg, #64748b, #94a3b8); } +.pug-stat-garansi::before { background: linear-gradient(90deg, var(--pug-rose), #f87171); } + +.pug-stat-label { + font-family: var(--pug-font); font-size: 10px; font-weight: 700; + letter-spacing: .1em; text-transform: uppercase; + color: var(--pug-t3); margin-bottom: 12px; +} +.pug-stat-val { + font-family: var(--pug-font); font-size: 36px; font-weight: 800; + color: var(--pug-t1); line-height: 1; margin-bottom: 8px; +} +.pug-stat-icon { + position: absolute; right: 16px; top: 16px; + width: 40px; height: 40px; border-radius: 10px; + display: flex; align-items: center; justify-content: center; font-size: 16px; +} +.pug-stat-total .pug-stat-icon { background: var(--pug-cyan-l); color: var(--pug-cyan); } +.pug-stat-belum .pug-stat-icon { background: var(--pug-amber-l); color: var(--pug-amber); } +.pug-stat-proses .pug-stat-icon { background: var(--pug-violet-l); color: var(--pug-violet); } +.pug-stat-selesai .pug-stat-icon { background: var(--pug-green-l); color: var(--pug-green); } +.pug-stat-batal .pug-stat-icon { background: #f1f5f9; color: #64748b; } +.pug-stat-garansi .pug-stat-icon { background: var(--pug-rose-l); color: var(--pug-rose); } + +/* ── Content layout ── */ +.pug-main-content { + display: flex; + flex-direction: column; + gap: 20px; +} + +/* ── Toolbar filter ── */ +.pug-toolbar { + background: var(--pug-white); border: 1px solid var(--pug-border); + border-radius: 16px; padding: 20px 24px; + box-shadow: 0 1px 4px rgba(0,0,0,0.06); +} +.pug-toolbar-form { + display: flex; flex-wrap: wrap; align-items: flex-end; gap: 24px; +} + +.pug-toolbar-item { flex: 1; min-width: 200px; } +.pug-toolbar-item.item-chips { flex: 2; min-width: 350px; } +.pug-toolbar-item.item-btn { flex: 0 0 auto; min-width: auto; } + +.pug-field-label { + font-size: 11px; color: var(--pug-t3); font-weight: 700; + text-transform: uppercase; letter-spacing: .08em; + margin-bottom: 8px; display: block; +} +.pug-input { + width: 100%; background: var(--pug-bg); + border: 1px solid var(--pug-border); color: var(--pug-t1); + border-radius: 10px; padding: 11px 14px; + font-family: var(--pug-font); font-size: 13px; + outline: none; transition: all .2s; +} +.pug-input::placeholder { color: var(--pug-t4); } +.pug-input:focus { border-color: var(--pug-green); box-shadow: 0 0 0 3px rgba(26,122,74,0.1); } +.pug-input-icon { position: relative; } +.pug-input-icon .pug-input { padding-left: 38px; } +.pug-input-icon i { + position: absolute; left: 14px; top: 50%; + transform: translateY(-50%); color: var(--pug-t4); font-size: 12px; +} + +.pug-reset-btn { + height: 42px; padding: 0 18px; border-radius: 10px; + background: transparent; border: 1px solid var(--pug-border); + color: var(--pug-t3); font-size: 12px; font-family: var(--pug-font); + font-weight: 600; cursor: pointer; transition: all .2s; + display: flex; align-items: center; justify-content: center; gap: 8px; +} +.pug-reset-btn:hover { border-color: var(--pug-rose); color: var(--pug-rose); background: var(--pug-rose-l); } + +.pug-chip-group { display: flex; flex-wrap: wrap; gap: 6px; } +.pug-chip { + padding: 8px 14px; border-radius: 10px; font-size: 11px; font-weight: 700; + border: 1px solid var(--pug-border); color: var(--pug-t2); + background: var(--pug-bg); cursor: pointer; transition: all .2s; font-family: var(--pug-font); +} +.pug-chip:hover, +.pug-chip.active { border-color: var(--pug-green); color: var(--pug-green); background: var(--pug-green-l); } +.pug-chip.amber:hover, +.pug-chip.amber.active { border-color: var(--pug-amber); color: var(--pug-amber); background: var(--pug-amber-l); } +.pug-chip.violet:hover, +.pug-chip.violet.active { border-color: var(--pug-violet); color: var(--pug-violet); background: var(--pug-violet-l); } +.pug-chip.rose:hover, +.pug-chip.rose.active { border-color: var(--pug-rose); color: var(--pug-rose); background: var(--pug-rose-l); } + + +/* ── Table panel ── */ +.pug-panel { + background: var(--pug-white); border: 1px solid var(--pug-border); + border-radius: 16px; overflow: hidden; + box-shadow: 0 1px 4px rgba(0,0,0,0.06); +} +.pug-panel-head { + padding: 18px 24px; border-bottom: 1px solid var(--pug-border); + display: flex; align-items: center; justify-content: space-between; + background: #f8fdf9; +} +.pug-panel-head-left { + font-family: var(--pug-font); font-size: 13px; + font-weight: 700; color: var(--pug-t1); + display: flex; align-items: center; gap: 8px; +} +.pug-panel-head-right { font-size: 11px; color: var(--pug-t3); } + +.pug-table { width: 100%; border-collapse: collapse; } +.pug-table thead tr { background: #f8fdf9; } +.pug-table th { + padding: 13px 20px; text-align: left; + font-family: var(--pug-font); font-size: 10px; font-weight: 700; + letter-spacing: .1em; text-transform: uppercase; color: var(--pug-t3); + border-bottom: 1px solid var(--pug-border); +} +.pug-table th:last-child { text-align: center; } +.pug-table td { + padding: 14px 20px; border-bottom: 1px solid #f0f7f3; + vertical-align: middle; font-family: var(--pug-font); +} +.pug-table tr:last-child td { border-bottom: none; } +.pug-table tbody tr { transition: background .15s; } +.pug-table tbody tr:hover { background: #f8fdf9; } + +.pug-rownum { font-size: 11px; color: var(--pug-t3); font-weight: 500; } + +.pug-av { + width: 38px; height: 38px; border-radius: 10px; flex-shrink: 0; + background: linear-gradient(135deg, var(--pug-green), var(--pug-green-m)); + color: #fff; font-size: 11px; font-weight: 700; + display: flex; align-items: center; justify-content: center; + cursor: pointer; z-index: 10; +} + +/* ── Searchable Tag Multi-Select ── */ +.pug-tag-select { + position: relative; width: 100%; +} +.pug-tag-input-wrapper { + display: flex; flex-wrap: wrap; gap: 6px; align-items: center; + padding: 8px 12px; background: #fff; + border: 1px solid var(--line); border-radius: 12px; + min-height: 48px; transition: all .2s; + cursor: text; +} +.pug-tag-input-wrapper:focus-within { + border-color: var(--pug-green); + box-shadow: 0 0 0 3px rgba(26,122,74,0.08); +} +.pug-tags-container { + display: flex; flex-wrap: wrap; gap: 6px; +} +.pug-tag { + background: var(--pug-green-l); color: var(--pug-green); + font-size: 12px; font-weight: 700; padding: 4px 10px; + border-radius: 6px; display: flex; align-items: center; gap: 6px; + border: 1px solid rgba(26,122,74,0.1); +} +.pug-tag i { cursor: pointer; opacity: 0.7; } +.pug-tag i:hover { opacity: 1; } + +.pug-tag-input { + border: none; outline: none; background: transparent; + font-size: 13.5px; color: var(--t1); flex: 1; + min-width: 120px; padding: 4px 0; +} + +.pug-tag-dropdown { + position: absolute; top: calc(100% + 5px); left: 0; right: 0; + background: #fff; border: 1px solid var(--line); + border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.1); + z-index: 100; max-height: 220px; overflow-y: auto; + display: none; +} +.pug-tag-dropdown.show { display: block; } + +.pug-tag-option { + padding: 10px 15px; cursor: pointer; transition: background .2s; + border-bottom: 1px solid #f8fafc; +} +.pug-tag-option:last-child { border-bottom: none; } +.pug-tag-option:hover { background: #f0fdf4; } +.pug-tag-option strong { display: block; font-size: 13px; color: var(--t1); } +.pug-tag-option span { font-size: 11px; color: var(--t3); } + +.pug-tag-no-results { + padding: 20px; text-align: center; color: var(--t3); font-size: 12px; + display: none; +} + +/* Custom Scrollbar for dropdown */ +.pug-tag-dropdown::-webkit-scrollbar { width: 6px; } +.pug-tag-dropdown::-webkit-scrollbar-track { background: transparent; } +.pug-tag-dropdown::-webkit-scrollbar-thumb { background: var(--line2); border-radius: 10px; } + +/* ── Technician Badge in Table ── */ +.pug-tech-badge { + display: inline-flex; align-items: center; gap: 4px; + background: #f8fafc; border: 1px solid var(--line); + padding: 3px 8px; border-radius: 6px; + font-size: 11px; font-weight: 600; color: var(--t1); + white-space: nowrap; transition: all .2s; +} +.pug-tech-badge i { font-size: 9px; color: var(--t3); } +.pug-tech-badge:hover { background: #fff; border-color: var(--pug-green); } + + + +.pug-av-name { font-size: 13px; font-weight: 600; color: var(--pug-t1); font-family: var(--pug-font); } +.pug-av-sub { font-size: 11px; color: var(--pug-t3); margin-top: 2px; } + +.pug-thumb { + width: 52px; height: 52px; border-radius: 9px; + object-fit: cover; cursor: pointer; + border: 1px solid var(--pug-border); transition: all .2s; +} +.pug-thumb:hover { border-color: var(--pug-green); transform: scale(1.08); } + +.pug-date { font-size: 12px; color: var(--pug-t1); font-weight: 500; } +.pug-note { font-size: 11px; color: var(--pug-t2); margin-top: 4px; display: flex; align-items: center; gap: 5px; } + +.pug-badge { + display: inline-flex; align-items: center; gap: 5px; + padding: 4px 10px; border-radius: 99px; font-size: 11px; font-weight: 700; + font-family: var(--pug-font); border: 1px solid; white-space: nowrap; +} +.pug-badge-green { background: var(--pug-green-l); color: var(--pug-green); border-color: var(--pug-green-l2); } +.pug-badge-amber { background: var(--pug-amber-l); color: var(--pug-amber); border-color: #fde68a; } +.pug-badge-violet { background: var(--pug-violet-l); color: var(--pug-violet); border-color: #ddd6fe; } +.pug-badge-rose { background: var(--pug-rose-l); color: var(--pug-rose); border-color: #fca5a5; } +.pug-badge-muted { background: #f8fafc; color: var(--pug-t3); border-color: var(--pug-border); } +.pug-badge-cyan { background: var(--pug-cyan-l); color: var(--pug-cyan); border-color: #a5f3fc; } + +.pug-money { font-size: 13px; font-weight: 700; color: var(--pug-green); } +.pug-money-empty { color: var(--pug-t4); font-size: 13px; } + +.pug-actions { display: flex; align-items: center; justify-content: center; gap: 6px; } +.pug-action-btn { + width: 32px; height: 32px; border-radius: 8px; + border: 1px solid var(--pug-border); background: transparent; + display: flex; align-items: center; justify-content: center; + cursor: pointer; transition: all .2s; font-size: 12px; +} +.pug-action-view:hover { background: var(--pug-cyan-l); border-color: var(--pug-cyan); color: var(--pug-cyan); } +.pug-action-view { color: var(--pug-cyan); } +.pug-action-edit { color: var(--pug-green); } +.pug-action-edit:hover { background: var(--pug-green-l); border-color: var(--pug-green); color: var(--pug-green); } +.pug-action-del { color: var(--pug-rose); } +.pug-action-del:hover { background: var(--pug-rose-l); border-color: var(--pug-rose); color: var(--pug-rose); } + +.pug-empty { padding: 80px 20px; text-align: center; } +.pug-empty-icon { + width: 72px; height: 72px; border-radius: 18px; + background: var(--pug-green-l); border: 1px solid var(--pug-green-l2); + margin: 0 auto 20px; display: flex; align-items: center; justify-content: center; + font-size: 28px; color: var(--pug-green); +} +.pug-empty-title { font-family: var(--pug-font); font-size: 15px; font-weight: 700; color: var(--pug-t2); margin-bottom: 6px; } +.pug-empty-sub { font-size: 12px; color: var(--pug-t3); } + +.pug-pag { padding: 16px 24px; border-top: 1px solid var(--pug-border); background: #f8fdf9; } + +/* ── Modals ── */ +.pug-overlay { + display: none; position: fixed; inset: 0; + background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); + z-index: 50; overflow-y: auto; +} +.pug-overlay.show { display: flex; align-items: flex-start; justify-content: center; padding: 40px 16px; } + +.pug-modal { + background: var(--pug-white); border: 1px solid var(--pug-border); + border-radius: 20px; width: 100%; padding: 32px; + box-shadow: 0 24px 60px rgba(0,0,0,0.15); + animation: pugModalIn .25s cubic-bezier(.34,1.56,.64,1); +} +@keyframes pugModalIn { + from { opacity:0; transform: scale(.94) translateY(12px); } + to { opacity:1; transform: none; } +} +.pug-modal-sm { max-width: 680px; } +.pug-modal-lg { max-width: 860px; } + +.pug-modal-header { + display: flex; align-items: center; justify-content: space-between; + margin-bottom: 28px; padding-bottom: 20px; border-bottom: 1px solid var(--pug-border); +} +.pug-modal-title { + font-family: var(--pug-font); font-size: 18px; font-weight: 700; + color: var(--pug-t1); display: flex; align-items: center; gap: 10px; +} +.pug-modal-title i { color: var(--pug-green); } +.pug-close-btn { + width: 34px; height: 34px; border-radius: 9px; + border: 1px solid var(--pug-border); color: var(--pug-t3); + background: transparent; cursor: pointer; + display: flex; align-items: center; justify-content: center; + font-size: 13px; transition: all .2s; +} +.pug-close-btn:hover { border-color: var(--pug-rose); color: var(--pug-rose); } + +.pug-info-box { + background: var(--pug-cyan-l); border: 1px solid #a5f3fc; + border-radius: 12px; padding: 14px 16px; margin-bottom: 24px; + display: flex; gap: 12px; align-items: flex-start; +} +.pug-info-box i { color: var(--pug-cyan); margin-top: 2px; flex-shrink: 0; } +.pug-info-box p { font-size: 13px; color: var(--pug-t2); line-height: 1.6; font-family: var(--pug-font); } +.pug-info-box strong { color: var(--pug-t1); } + +.pug-form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } +@media(max-width:600px) { .pug-form-grid { grid-template-columns: 1fr; } } +.pug-form-full { grid-column: 1/-1; } + +.pug-form-field { display: flex; flex-direction: column; gap: 7px; } +.pug-form-label { + font-size: 11px; font-weight: 700; letter-spacing: .06em; + text-transform: uppercase; color: var(--pug-t2); + display: flex; align-items: center; gap: 6px; +} +.pug-form-label i { color: var(--pug-green); font-size: 10px; } +.pug-form-label .req { color: var(--pug-rose); } +.pug-form-label .opt { color: var(--pug-t4); font-weight: 400; text-transform: none; letter-spacing: 0; font-size: 10px; } + +.pug-select, .pug-text-input, .pug-textarea { + background: var(--pug-bg); border: 1px solid var(--pug-border); + color: var(--pug-t1); border-radius: 10px; padding: 11px 14px; + font-family: var(--pug-font); font-size: 13px; + outline: none; transition: border-color .2s, box-shadow .2s; width: 100%; +} +.pug-select:focus, .pug-text-input:focus, .pug-textarea:focus { + border-color: var(--pug-green); + box-shadow: 0 0 0 3px rgba(26,122,74,0.08); +} +.pug-text-input::placeholder, .pug-textarea::placeholder { color: var(--pug-t4); } +.pug-textarea { resize: none; } + +.pug-upload-zone { + border: 1.5px dashed var(--pug-border); border-radius: 12px; + padding: 36px 20px; text-align: center; cursor: pointer; + background: var(--pug-bg); transition: all .2s; +} +.pug-upload-zone:hover { border-color: var(--pug-green); background: var(--pug-green-l); } +.pug-upload-zone i { font-size: 32px; color: var(--pug-t4); display: block; margin-bottom: 10px; } +.pug-upload-zone p { font-size: 13px; color: var(--pug-t2); font-weight: 500; margin-bottom: 4px; } +.pug-upload-zone span { font-size: 11px; color: var(--pug-t4); } + +.pug-preview-wrap { position: relative; } +.pug-preview-img { + width: 100%; max-height: 240px; object-fit: contain; + border-radius: 10px; border: 1px solid var(--pug-green-l2); +} +.pug-preview-remove { + position: absolute; top: 8px; right: 8px; + width: 28px; height: 28px; border-radius: 50%; + background: var(--pug-rose); color: #fff; border: none; + display: flex; align-items: center; justify-content: center; + font-size: 11px; cursor: pointer; +} +.pug-fname { font-size: 11px; color: var(--pug-t3); text-align: center; margin-top: 6px; } + +.pug-old-img-label { font-size: 11px; color: var(--pug-t3); margin-bottom: 6px; } +.pug-old-img { width: 80px; height: 80px; object-fit: cover; border-radius: 9px; border: 1px solid var(--pug-border); } + +.pug-modal-footer { + display: flex; align-items: center; justify-content: flex-end; gap: 10px; + padding-top: 24px; margin-top: 8px; border-top: 1px solid var(--pug-border); +} +.pug-btn-cancel { + padding: 10px 20px; border-radius: 10px; font-size: 13px; font-weight: 600; + font-family: var(--pug-font); background: transparent; + border: 1px solid var(--pug-border); color: var(--pug-t2); cursor: pointer; + display: flex; align-items: center; gap: 7px; transition: all .2s; +} +.pug-btn-cancel:hover { border-color: var(--pug-rose); color: var(--pug-rose); } +.pug-btn-submit { + padding: 10px 24px; border-radius: 10px; font-size: 13px; font-weight: 700; + font-family: var(--pug-font); + background: linear-gradient(135deg, var(--pug-green), var(--pug-green-m)); + color: #fff; border: none; cursor: pointer; + display: flex; align-items: center; gap: 7px; transition: all .2s; + box-shadow: 0 4px 14px rgba(26,122,74,0.25); +} +.pug-btn-submit:hover { opacity: .9; transform: translateY(-1px); } + +/* Detail modal */ +.pug-detail-section { + background: var(--pug-bg); border: 1px solid var(--pug-border); + border-radius: 12px; padding: 18px; margin-bottom: 14px; +} +.pug-detail-section-title { + font-family: var(--pug-font); font-size: 10px; font-weight: 700; + letter-spacing: .1em; text-transform: uppercase; color: var(--pug-t3); + margin-bottom: 14px; display: flex; align-items: center; gap: 7px; +} +.pug-detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; } +.pug-detail-item-label { font-size: 10px; color: var(--pug-t3); margin-bottom: 3px; text-transform: uppercase; letter-spacing: .06em; } +.pug-detail-item-val { font-size: 13px; color: var(--pug-t1); font-weight: 600; } +.pug-detail-money { font-size: 18px; color: var(--pug-green); font-weight: 700; font-family: var(--pug-font); } + +.pug-garansi-box { + background: var(--pug-violet-l); border: 1px solid #ddd6fe; + border-radius: 12px; padding: 14px 16px; + display: flex; gap: 12px; align-items: center; margin-bottom: 14px; +} +.pug-garansi-box i { color: var(--pug-violet); font-size: 22px; } + +.pug-waiting-box { + background: var(--pug-amber-l); border: 1px solid #fde68a; + border-radius: 12px; padding: 14px 16px; + display: flex; gap: 12px; align-items: center; margin-bottom: 14px; +} +.pug-waiting-box i { color: var(--pug-amber); font-size: 22px; } + +.pug-photo-container { background: #fff; border-radius: 12px; padding: 10px; border: 1px solid var(--pug-border); } +.pug-photo-label { font-size: 10px; font-weight: 700; color: var(--pug-t2); text-transform: uppercase; margin-bottom: 8px; display: flex; align-items: center; gap: 5px; } +.pug-photo-item { width: 100%; height: 160px; object-fit: cover; border-radius: 8px; cursor: pointer; transition: transform .2s; border: 1px solid #f0f7f3; } +.pug-photo-item:hover { transform: scale(1.02); } +.pug-photo-empty { height: 180px; display: flex; align-items: center; justify-content: center; background: #f8fafc; border: 1px dashed var(--pug-border); border-radius: 12px; color: var(--pug-t3); font-size: 11px; } + +.pug-preview-overlay { + display: none; position: fixed; inset: 0; + background: rgba(0,0,0,.92); backdrop-filter: blur(10px); + z-index: 60; align-items: center; justify-content: center; +} +.pug-preview-overlay.show { display: flex; } +.pug-preview-overlay img { + max-width: 90vw; max-height: 90vh; object-fit: contain; + border-radius: 14px; border: 1px solid rgba(255,255,255,0.1); +} +.pug-preview-x { + position: absolute; top: 20px; right: 20px; + width: 40px; height: 40px; border-radius: 50%; + background: var(--pug-rose); color: #fff; border: none; + display: flex; align-items: center; justify-content: center; + font-size: 15px; cursor: pointer; +} \ No newline at end of file diff --git a/samooapk/public/HYqsyHtjImI6cItJKoBEBbI31JcRcqD7mU9ltXS6.jpg b/samooapk/public/HYqsyHtjImI6cItJKoBEBbI31JcRcqD7mU9ltXS6.jpg new file mode 100644 index 0000000..cc984fb Binary files /dev/null and b/samooapk/public/HYqsyHtjImI6cItJKoBEBbI31JcRcqD7mU9ltXS6.jpg differ diff --git a/samooapk/public/Il8Qxd8WfMKGVGYJ36cJvOvbevbKKyCpdgG5KaaE.jpg b/samooapk/public/Il8Qxd8WfMKGVGYJ36cJvOvbevbKKyCpdgG5KaaE.jpg new file mode 100644 index 0000000..bfd7aba Binary files /dev/null and b/samooapk/public/Il8Qxd8WfMKGVGYJ36cJvOvbevbKKyCpdgG5KaaE.jpg differ diff --git a/samooapk/public/JTb1T4xVqpwlxvM9kkg92prqgcNmYbPiqZsNIKYL.jpg b/samooapk/public/JTb1T4xVqpwlxvM9kkg92prqgcNmYbPiqZsNIKYL.jpg new file mode 100644 index 0000000..d14c506 Binary files /dev/null and b/samooapk/public/JTb1T4xVqpwlxvM9kkg92prqgcNmYbPiqZsNIKYL.jpg differ diff --git a/samooapk/public/JdYxfUPCXETtMBH4TinTuo1lVYFWaNaEf5Fs0j2L.jpg b/samooapk/public/JdYxfUPCXETtMBH4TinTuo1lVYFWaNaEf5Fs0j2L.jpg new file mode 100644 index 0000000..790424b Binary files /dev/null and b/samooapk/public/JdYxfUPCXETtMBH4TinTuo1lVYFWaNaEf5Fs0j2L.jpg differ diff --git a/samooapk/public/JsGHTVUNH3yxzMYbeCiIVXiHsIucEPhMfGDKopLn.jpg b/samooapk/public/JsGHTVUNH3yxzMYbeCiIVXiHsIucEPhMfGDKopLn.jpg new file mode 100644 index 0000000..80bb8f2 Binary files /dev/null and b/samooapk/public/JsGHTVUNH3yxzMYbeCiIVXiHsIucEPhMfGDKopLn.jpg differ diff --git a/samooapk/public/NgbGtWueOf2WRrgQFgzi5JHZATsgjdkQMdbY4e1t.jpg b/samooapk/public/NgbGtWueOf2WRrgQFgzi5JHZATsgjdkQMdbY4e1t.jpg new file mode 100644 index 0000000..d3c516c Binary files /dev/null and b/samooapk/public/NgbGtWueOf2WRrgQFgzi5JHZATsgjdkQMdbY4e1t.jpg differ diff --git a/samooapk/public/Oj1R8U2QUFVvRol8pOdCBH0aadvh5KLLcGkDXxbg.jpg b/samooapk/public/Oj1R8U2QUFVvRol8pOdCBH0aadvh5KLLcGkDXxbg.jpg new file mode 100644 index 0000000..bfd7aba Binary files /dev/null and b/samooapk/public/Oj1R8U2QUFVvRol8pOdCBH0aadvh5KLLcGkDXxbg.jpg differ diff --git a/samooapk/public/QnoYyWN5EqRNQYJ8Vy7dq2KY6AF0C2d7xGm3Fn3m.jpg b/samooapk/public/QnoYyWN5EqRNQYJ8Vy7dq2KY6AF0C2d7xGm3Fn3m.jpg new file mode 100644 index 0000000..4cf8632 Binary files /dev/null and b/samooapk/public/QnoYyWN5EqRNQYJ8Vy7dq2KY6AF0C2d7xGm3Fn3m.jpg differ diff --git a/samooapk/public/VrJAHhWZDdnjk021euY4ytaJSs19gMQ6Kzv0QDcD.jpg b/samooapk/public/VrJAHhWZDdnjk021euY4ytaJSs19gMQ6Kzv0QDcD.jpg new file mode 100644 index 0000000..80bb8f2 Binary files /dev/null and b/samooapk/public/VrJAHhWZDdnjk021euY4ytaJSs19gMQ6Kzv0QDcD.jpg differ diff --git a/samooapk/public/WENrW8RS6cTlwMQUIdJvRd7lrkt3QLEVDUInv97s.jpg b/samooapk/public/WENrW8RS6cTlwMQUIdJvRd7lrkt3QLEVDUInv97s.jpg new file mode 100644 index 0000000..3251ca1 Binary files /dev/null and b/samooapk/public/WENrW8RS6cTlwMQUIdJvRd7lrkt3QLEVDUInv97s.jpg differ diff --git a/samooapk/public/Yur4BG7etQCfG0wX9i4SxwIdAaBmMqd5SVmmARuY.jpg b/samooapk/public/Yur4BG7etQCfG0wX9i4SxwIdAaBmMqd5SVmmARuY.jpg new file mode 100644 index 0000000..4cf8632 Binary files /dev/null and b/samooapk/public/Yur4BG7etQCfG0wX9i4SxwIdAaBmMqd5SVmmARuY.jpg differ diff --git a/samooapk/public/bGM0FZ4EZk9NWLnXlmyZ17pVxvgblAr0lilOdBfE.jpg b/samooapk/public/bGM0FZ4EZk9NWLnXlmyZ17pVxvgblAr0lilOdBfE.jpg new file mode 100644 index 0000000..bfd7aba Binary files /dev/null and b/samooapk/public/bGM0FZ4EZk9NWLnXlmyZ17pVxvgblAr0lilOdBfE.jpg differ diff --git a/samooapk/public/clear.php b/samooapk/public/clear.php new file mode 100644 index 0000000..115145d --- /dev/null +++ b/samooapk/public/clear.php @@ -0,0 +1,7 @@ +make(Illuminate\Contracts\Console\Kernel::class); +$kernel->call('config:clear'); +$kernel->call('cache:clear'); +echo 'Cache cleared!'; \ No newline at end of file diff --git a/samooapk/public/favicon.ico b/samooapk/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/samooapk/public/fgrMGqtty8OjfoGS5fnACK4AZwO0WZMxwPQ0Njko.jpg b/samooapk/public/fgrMGqtty8OjfoGS5fnACK4AZwO0WZMxwPQ0Njko.jpg new file mode 100644 index 0000000..4cf8632 Binary files /dev/null and b/samooapk/public/fgrMGqtty8OjfoGS5fnACK4AZwO0WZMxwPQ0Njko.jpg differ diff --git a/samooapk/public/hgw680i0F88ixtZgq4wXwsce45XPChdn79jyMH0O.jpg b/samooapk/public/hgw680i0F88ixtZgq4wXwsce45XPChdn79jyMH0O.jpg new file mode 100644 index 0000000..bfd7aba Binary files /dev/null and b/samooapk/public/hgw680i0F88ixtZgq4wXwsce45XPChdn79jyMH0O.jpg differ diff --git a/samooapk/public/hrS5vRS1eqphHvzC9CQGUvIRZg74gWQ4CEvV32EJ.jpg b/samooapk/public/hrS5vRS1eqphHvzC9CQGUvIRZg74gWQ4CEvV32EJ.jpg new file mode 100644 index 0000000..3ca2a3d Binary files /dev/null and b/samooapk/public/hrS5vRS1eqphHvzC9CQGUvIRZg74gWQ4CEvV32EJ.jpg differ diff --git a/samooapk/public/images/SIPDAM.png b/samooapk/public/images/SIPDAM.png new file mode 100644 index 0000000..7b38c91 Binary files /dev/null and b/samooapk/public/images/SIPDAM.png differ diff --git a/samooapk/public/images/hero-right.png b/samooapk/public/images/hero-right.png new file mode 100644 index 0000000..3a6ccd0 Binary files /dev/null and b/samooapk/public/images/hero-right.png differ diff --git a/samooapk/public/images/logo tirta sanjiwani.png b/samooapk/public/images/logo tirta sanjiwani.png new file mode 100644 index 0000000..896e58d Binary files /dev/null and b/samooapk/public/images/logo tirta sanjiwani.png differ diff --git a/samooapk/public/index.php b/samooapk/public/index.php new file mode 100644 index 0000000..1d69f3a --- /dev/null +++ b/samooapk/public/index.php @@ -0,0 +1,55 @@ +make(Kernel::class); + +$response = $kernel->handle( + $request = Request::capture() +)->send(); + +$kernel->terminate($request, $response); diff --git a/samooapk/public/j8gRWvHreru54sbhCLgTNlrvm94TkETGA3ltKfja.jpg b/samooapk/public/j8gRWvHreru54sbhCLgTNlrvm94TkETGA3ltKfja.jpg new file mode 100644 index 0000000..14471a8 Binary files /dev/null and b/samooapk/public/j8gRWvHreru54sbhCLgTNlrvm94TkETGA3ltKfja.jpg differ diff --git a/samooapk/public/js/dashboard.js b/samooapk/public/js/dashboard.js new file mode 100644 index 0000000..a3d8513 --- /dev/null +++ b/samooapk/public/js/dashboard.js @@ -0,0 +1,62 @@ +// Dashboard JavaScript +document.addEventListener('DOMContentLoaded', function() { + const sidebar = document.getElementById('sidebar'); + const mainContent = document.getElementById('mainContent'); + const sidebarToggle = document.getElementById('sidebarToggle'); + const sidebarOverlay = document.getElementById('sidebarOverlay'); + + // Toggle sidebar + sidebarToggle.addEventListener('click', function() { + sidebar.classList.toggle('active'); + sidebarOverlay.classList.toggle('active'); + }); + + // Close sidebar when clicking overlay + sidebarOverlay.addEventListener('click', function() { + sidebar.classList.remove('active'); + sidebarOverlay.classList.remove('active'); + }); + + // Close sidebar when clicking nav links (mobile only) + const navLinks = document.querySelectorAll('.sidebar .nav-link'); + navLinks.forEach(link => { + link.addEventListener('click', function() { + if (window.innerWidth < 768) { + sidebar.classList.remove('active'); + sidebarOverlay.classList.remove('active'); + } + }); + }); + + // Handle window resize + window.addEventListener('resize', function() { + if (window.innerWidth >= 768) { + sidebar.classList.remove('active'); + sidebarOverlay.classList.remove('active'); + } + }); + + // Add smooth scrolling for internal links + document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', function (e) { + e.preventDefault(); + const target = document.querySelector(this.getAttribute('href')); + if (target) { + target.scrollIntoView({ + behavior: 'smooth' + }); + } + }); + }); + + // Auto-hide alerts after 5 seconds + const alerts = document.querySelectorAll('.alert'); + alerts.forEach(alert => { + setTimeout(() => { + alert.style.opacity = '0'; + setTimeout(() => { + alert.remove(); + }, 300); + }, 5000); + }); +}); \ No newline at end of file diff --git a/samooapk/public/kWyIwBStVGk1MF0dALIAYzeBzyOW5yVMQfnS56zE.jpg b/samooapk/public/kWyIwBStVGk1MF0dALIAYzeBzyOW5yVMQfnS56zE.jpg new file mode 100644 index 0000000..790424b Binary files /dev/null and b/samooapk/public/kWyIwBStVGk1MF0dALIAYzeBzyOW5yVMQfnS56zE.jpg differ diff --git a/samooapk/public/mvYyr3PBgpjn9h6JT8T5znCM9IYW0EHGNTDiOUYB.jpg b/samooapk/public/mvYyr3PBgpjn9h6JT8T5znCM9IYW0EHGNTDiOUYB.jpg new file mode 100644 index 0000000..d60498c Binary files /dev/null and b/samooapk/public/mvYyr3PBgpjn9h6JT8T5znCM9IYW0EHGNTDiOUYB.jpg differ diff --git a/samooapk/public/n7LuqegfnuVcW8TxXo7KedKjs2SdR8BQFfDntH3P.jpg b/samooapk/public/n7LuqegfnuVcW8TxXo7KedKjs2SdR8BQFfDntH3P.jpg new file mode 100644 index 0000000..d3c516c Binary files /dev/null and b/samooapk/public/n7LuqegfnuVcW8TxXo7KedKjs2SdR8BQFfDntH3P.jpg differ diff --git a/samooapk/public/nU6caSqFnSafFHGdSkMX85GbLz0wy3EJqvRE2gI0.png b/samooapk/public/nU6caSqFnSafFHGdSkMX85GbLz0wy3EJqvRE2gI0.png new file mode 100644 index 0000000..c026050 Binary files /dev/null and b/samooapk/public/nU6caSqFnSafFHGdSkMX85GbLz0wy3EJqvRE2gI0.png differ diff --git a/samooapk/public/nnF8QYnxqdWnj9AcMsMWWCD59ELWozKPBnOXpMDU.jpg b/samooapk/public/nnF8QYnxqdWnj9AcMsMWWCD59ELWozKPBnOXpMDU.jpg new file mode 100644 index 0000000..8bffb64 Binary files /dev/null and b/samooapk/public/nnF8QYnxqdWnj9AcMsMWWCD59ELWozKPBnOXpMDU.jpg differ diff --git a/samooapk/public/oCXslHX3h5WuMt8BkEcJ9dHscPCSZ6VgWcJtk6pK.jpg b/samooapk/public/oCXslHX3h5WuMt8BkEcJ9dHscPCSZ6VgWcJtk6pK.jpg new file mode 100644 index 0000000..d60498c Binary files /dev/null and b/samooapk/public/oCXslHX3h5WuMt8BkEcJ9dHscPCSZ6VgWcJtk6pK.jpg differ diff --git a/samooapk/public/ph0DxHdof9PoZaGdjnhlMMvyDGFDMYB8j6QUlAF2.jpg b/samooapk/public/ph0DxHdof9PoZaGdjnhlMMvyDGFDMYB8j6QUlAF2.jpg new file mode 100644 index 0000000..3251ca1 Binary files /dev/null and b/samooapk/public/ph0DxHdof9PoZaGdjnhlMMvyDGFDMYB8j6QUlAF2.jpg differ diff --git a/samooapk/public/robots.txt b/samooapk/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/samooapk/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/samooapk/public/u5ud0g7yfUL6Z6HEB7jxaVKYHe7A9CVUAwCVOrMM.jpg b/samooapk/public/u5ud0g7yfUL6Z6HEB7jxaVKYHe7A9CVUAwCVOrMM.jpg new file mode 100644 index 0000000..4cf8632 Binary files /dev/null and b/samooapk/public/u5ud0g7yfUL6Z6HEB7jxaVKYHe7A9CVUAwCVOrMM.jpg differ diff --git a/samooapk/public/u9QMtuaP8unIIc90pMSVRLgrUg6smHpWPsVC6ewD.jpg b/samooapk/public/u9QMtuaP8unIIc90pMSVRLgrUg6smHpWPsVC6ewD.jpg new file mode 100644 index 0000000..14471a8 Binary files /dev/null and b/samooapk/public/u9QMtuaP8unIIc90pMSVRLgrUg6smHpWPsVC6ewD.jpg differ diff --git a/samooapk/public/uZolJZGlt0MpNVKHvAwuqNmFb9eHS37IvcDWeU9S.jpg b/samooapk/public/uZolJZGlt0MpNVKHvAwuqNmFb9eHS37IvcDWeU9S.jpg new file mode 100644 index 0000000..4cf8632 Binary files /dev/null and b/samooapk/public/uZolJZGlt0MpNVKHvAwuqNmFb9eHS37IvcDWeU9S.jpg differ diff --git a/samooapk/public/whZe3GYmPMR0NmilBmRp3KTTCgPRAOcFsgkePsNR.jpg b/samooapk/public/whZe3GYmPMR0NmilBmRp3KTTCgPRAOcFsgkePsNR.jpg new file mode 100644 index 0000000..3ca2a3d Binary files /dev/null and b/samooapk/public/whZe3GYmPMR0NmilBmRp3KTTCgPRAOcFsgkePsNR.jpg differ diff --git a/samooapk/public/yIgdFdNn6OkGmFuvm8oEx1ZGlioFMZHU3Vw4YooV.jpg b/samooapk/public/yIgdFdNn6OkGmFuvm8oEx1ZGlioFMZHU3Vw4YooV.jpg new file mode 100644 index 0000000..d14c506 Binary files /dev/null and b/samooapk/public/yIgdFdNn6OkGmFuvm8oEx1ZGlioFMZHU3Vw4YooV.jpg differ diff --git a/samooapk/resources/css/app.css b/samooapk/resources/css/app.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/samooapk/resources/css/app.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/samooapk/resources/js/app.js b/samooapk/resources/js/app.js new file mode 100644 index 0000000..a8093be --- /dev/null +++ b/samooapk/resources/js/app.js @@ -0,0 +1,7 @@ +import './bootstrap'; + +import Alpine from 'alpinejs'; + +window.Alpine = Alpine; + +Alpine.start(); diff --git a/samooapk/resources/js/bootstrap.js b/samooapk/resources/js/bootstrap.js new file mode 100644 index 0000000..846d350 --- /dev/null +++ b/samooapk/resources/js/bootstrap.js @@ -0,0 +1,32 @@ +/** + * We'll load the axios HTTP library which allows us to easily issue requests + * to our Laravel back-end. This library automatically handles sending the + * CSRF token as a header based on the value of the "XSRF" token cookie. + */ + +import axios from 'axios'; +window.axios = axios; + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; + +/** + * Echo exposes an expressive API for subscribing to channels and listening + * for events that are broadcast by Laravel. Echo and event broadcasting + * allows your team to easily build robust real-time web applications. + */ + +// import Echo from 'laravel-echo'; + +// import Pusher from 'pusher-js'; +// window.Pusher = Pusher; + +// window.Echo = new Echo({ +// broadcaster: 'pusher', +// key: import.meta.env.VITE_PUSHER_APP_KEY, +// cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1', +// wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`, +// wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80, +// wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443, +// forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https', +// enabledTransports: ['ws', 'wss'], +// }); diff --git a/samooapk/resources/views/Admin/Gaji/Kasbon.blade.php b/samooapk/resources/views/Admin/Gaji/Kasbon.blade.php new file mode 100644 index 0000000..e01c800 --- /dev/null +++ b/samooapk/resources/views/Admin/Gaji/Kasbon.blade.php @@ -0,0 +1,409 @@ + +@push('styles') + + +@endpush + +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+
Data Kasbon
+
Kelola pinjaman dan kasbon teknisi
+
+
+ +
+ + @if(session('success')) + + @endif + + {{-- ── DYNAMIC ALERT ── --}} + + + {{-- ── STATISTICS CARDS ── --}} +
+
+
+
TOTAL KASBON
+
{{ $totalKasbon ?? 0 }}
+
Semua catatan kasbon
+
+
+
+
LUNAS
+
{{ $kasbonLunas ?? 0 }}
+
Kasbon sudah diselesaikan
+
+
+
+
BELUM LUNAS
+
{{ $kasbonBelumLunas ?? 0 }}
+
Menunggu pelunasan
+
+
+
+
NOMINAL BELUM LUNAS
+
Rp {{ number_format($totalNominalBelumLunas ?? 0, 0, ',', '.') }}
+
Total hutang aktif
+
+
+
+
TOTAL NOMINAL
+
Rp {{ number_format($totalNominal ?? 0, 0, ',', '.') }}
+
Akumulasi seluruh kasbon
+
+
+ + {{-- ── TOOLBAR FILTER ── --}} +
+
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+
+ + {{-- ── DATA TABLE ── --}} +
+
+
+ + Daftar Kasbon +
+
+ Menampilkan {{ $kasbons->total() }} data kasbon +
+
+ +
+ + + + + + + + + + + + + + @forelse($kasbons as $index => $kasbon) + + + + + + + + + + @empty + + + + @endforelse + +
NoTeknisiTanggalJumlahStatusKeteranganAksi
+ {{ str_pad($kasbons->firstItem() + $index, 2, '0', STR_PAD_LEFT) }} + +
+
+ {{ strtoupper(substr($kasbon->teknisi->nama ?? 'T', 0, 1)) }} +
+
+
{{ $kasbon->teknisi->nama ?? 'Unknown' }}
+
+
+
+
{{ \Carbon\Carbon::parse($kasbon->tanggal_kasbon)->format('d/m/Y') }}
+
{{ \Carbon\Carbon::parse($kasbon->tanggal_kasbon)->isoFormat('dddd') }}
+
+
Rp {{ number_format($kasbon->jumlah_kasbon, 0, ',', '.') }}
+
+ @if($kasbon->status == 'lunas') + + Lunas + + @else + + Belum Lunas + + @endif + +
+ {{ $kasbon->keperluan ?? '-' }} +
+
+
+ + @if($kasbon->status == 'belum_lunas') + + @endif + +
+
+
+
+
Data Kasbon Tidak Ditemukan
+
Belum ada catatan kasbon untuk periode ini.
+
+
+
+ + @if($kasbons->hasPages()) +
+ {{ $kasbons->appends(request()->query())->links() }} +
+ @endif +
+
+
+ +{{-- ── MODAL TAMBAH/EDIT ── --}} +
+
+
+
Tambah Kasbon Baru
+ +
+ +
+ @csrf + + + +
+
+ + +
+ +
+ + +
+ +
+ +
+ Rp + +
+
+ + + +
+ + +
+
+ + +
+
+
+ +@push('scripts') + +@endpush +
\ No newline at end of file diff --git a/samooapk/resources/views/Admin/Gaji/Penggajian.blade.php b/samooapk/resources/views/Admin/Gaji/Penggajian.blade.php new file mode 100644 index 0000000..bebc716 --- /dev/null +++ b/samooapk/resources/views/Admin/Gaji/Penggajian.blade.php @@ -0,0 +1,547 @@ + +@push('styles') + +@endpush + +
+ + {{-- ALERT --}} + + + {{-- ── FORM PERIODE ── --}} +
+
+ + Pilih Periode Penggajian +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {{-- ── SUMMARY ── --}} + + + {{-- ── TABLE CARD ── --}} +
+
+
+ + Daftar Perhitungan Gaji +
+ +
+ + {{-- Loading --}} +
+
+
Sedang menghitung gaji teknisi...
+
+ + {{-- Empty state --}} +
+
💸
+
Belum ada data penggajian yang dimuat
+
Silakan pilih periode di atas lalu klik Hitung Gaji Otomatis
+
+ + {{-- Table --}} + +
+ +
+ +{{-- ── DETAIL MODAL ── --}} +
+
+
+
+ + Detail Perhitungan Gaji +
+ +
+
+
Memuat data…
+
+ +
+
+ +@push('scripts') + +@endpush +
\ No newline at end of file diff --git a/samooapk/resources/views/Admin/Gaji/detail_penggajian.blade.php b/samooapk/resources/views/Admin/Gaji/detail_penggajian.blade.php new file mode 100644 index 0000000..90ada67 --- /dev/null +++ b/samooapk/resources/views/Admin/Gaji/detail_penggajian.blade.php @@ -0,0 +1,137 @@ +
+
+
+
+ +
+
+
Slip Gaji Teknisi
+
+ Periode: {{ \App\Models\Penggajian::getNamaBulan($penggajian->periode_bulan) }} {{ $penggajian->periode_tahun }} +
+
+
+
+
Status
+ @if($penggajian->isPaid()) + LUNAS + @else + BELUM DIBAYAR + @endif +
+
+ +
+ +
+
Data Personel
+
+
+ Nama Lengkap + {{ $penggajian->teknisi->nama }} +
+
+ ID Teknisi + #{{ str_pad($penggajian->id_teknisi, 4, '0', STR_PAD_LEFT) }} +
+
+ Tgl. Hitung + {{ $penggajian->tanggal_penggajian->format('d M Y') }} +
+
+ Absensi Hadir + {{ $penggajian->jumlah_hari_kerja }} Hari +
+
+
+ + +
+
Summary Borongan
+
+
+ Jumlah Tugas + {{ $penggajian->detailPenggajian->count() }} Tugas +
+
+ Gaji Kotor + Rp {{ number_format($penggajian->total_ongkos_pekerjaan, 0, ',', '.') }} +
+
+ Potongan Makan +
+ - Rp {{ number_format($penggajian->biaya_makan, 0, ',', '.') }} + @if(!$penggajian->isPaid()) + + @endif +
+
+
+ Potongan Kasbon +
+ - Rp {{ number_format($penggajian->total_potongan ?? $penggajian->total_kasbon, 0, ',', '.') }} + @if(!$penggajian->isPaid()) + + @endif +
+
+
+
+
+ + +
+
+ Rincian Pembagian Ongkos Penugasan +
+
+ + + + + + + + + + @foreach($penggajian->detailPenggajian as $item) + + + + + + @endforeach + +
Tugas / LokasiTimBagian Anda
+
{{ $item->penugasan->label_jenis_pekerjaan ?? 'Tugas #'.$item->id_penugasan }}
+
{{ \Illuminate\Support\Str::limit($item->lokasi, 40) }}
+ @if($item->rincian_pekerjaan) +
+ {{ $item->rincian_pekerjaan }} +
+ @endif +
+ {{ $item->jumlah_tim }} Org + + Rp {{ number_format($item->bagian_ongkos, 0, ',', '.') }} +
+
+
+ + +
+
+
Take Home Pay
+
Total Gaji Bersih Terakhir
+
+
+
+ Rp {{ number_format($penggajian->gaji_bersih, 0, ',', '.') }} +
+
+
+
diff --git a/samooapk/resources/views/Admin/Gaji/slip_penggajian.blade.php b/samooapk/resources/views/Admin/Gaji/slip_penggajian.blade.php new file mode 100644 index 0000000..938a79d --- /dev/null +++ b/samooapk/resources/views/Admin/Gaji/slip_penggajian.blade.php @@ -0,0 +1,207 @@ + + + + + + Slip Gaji - {{ $penggajian->teknisi->nama }} + + + +
+
+

Slip Gaji Teknisi

+

PDAM Tirta Sanjiwani

+

Periode: {{ \Carbon\Carbon::create()->month($penggajian->periode_bulan)->translatedFormat('F') }} {{ $penggajian->periode_tahun }}

+
+ +
+ + + + +
Nama Teknisi:{{ $penggajian->teknisi->nama }}
ID / NIK:{{ $penggajian->teknisi->id_teknisi }}
Jabatan:{{ $penggajian->teknisi->spesialisasi ?? 'Teknisi Lapangan' }}
+ + + + +
No. Slip:SLP-{{ $penggajian->periode_tahun }}{{ str_pad($penggajian->periode_bulan, 2, '0', STR_PAD_LEFT) }}-{{ str_pad($penggajian->id_penggajian, 4, '0', STR_PAD_LEFT) }}
Tgl Cetak:{{ $penggajian->tanggal_penggajian->format('d/m/Y') }}
Status:{{ $penggajian->status_pembayaran == 'sudah_bayar' ? 'LUNAS' : 'BELUM DIBAYAR' }}
+
+ + + + + + + + + + + + + @if($penggajian->detailPenggajian && $penggajian->detailPenggajian->count() > 0) + @foreach($penggajian->detailPenggajian as $index => $item) + + + + + + + + @endforeach + @else + + + + @endif + +
NoRincian Penugasan SelesaiOngkos TugasTimBagian (Rp)
{{ $index + 1 }} + {{ $item->penugasan->label_jenis_pekerjaan ?? 'Tugas #'.$item->id_penugasan }}
+ {{ $item->lokasi }} + @if($item->rincian_pekerjaan) +
{{ $item->rincian_pekerjaan }} + @endif +
Rp {{ number_format($item->ongkos_penugasan, 0, ',', '.') }}{{ $item->jumlah_tim }}Rp {{ number_format($item->bagian_ongkos, 0, ',', '.') }}
Tidak ada data penugasan di bulan ini.
+ + + + + + + + + + + + + + + + + + +
Total Ongkos Pekerjaan{{ number_format($penggajian->total_ongkos_pekerjaan, 0, ',', '.') }}
Uang Makan ({{ $penggajian->jumlah_hari_kerja }} Hari Hadir)+ {{ number_format($penggajian->biaya_makan, 0, ',', '.') }}
Potongan Kasbon Berjalan- {{ number_format($penggajian->total_potongan ?? $penggajian->total_kasbon, 0, ',', '.') }}
Gaji Bersih DiterimaRp {{ number_format($penggajian->gaji_bersih, 0, ',', '.') }}
+ +
+
+

Mengetahui/Menyetujui,
Manajer Operasional

+
PDAM Tirta Sanjiwani
+
+
+

Diterima Oleh,
Teknisi Lapangan

+
{{ $penggajian->teknisi->nama }}
+
+
+
+ + diff --git a/samooapk/resources/views/Admin/KelolaPekerjaan/Penugasan.blade.php b/samooapk/resources/views/Admin/KelolaPekerjaan/Penugasan.blade.php new file mode 100644 index 0000000..3fafa65 --- /dev/null +++ b/samooapk/resources/views/Admin/KelolaPekerjaan/Penugasan.blade.php @@ -0,0 +1,809 @@ + + +@push('styles') + +@endpush +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+
Data Penugasan
+
Manajemen penugasan teknisi lapangan
+
+
+ +
+ + +
+
+ + @if(session('success')) + + @endif + @if(session('error')) + + @endif + + {{-- ── ABANDONED TASKS ALERT ── --}} + @if(isset($abandonedTasks) && $abandonedTasks->count() > 0) +
+
+ +
+
+

PERHATIAN: Ada Personel Tim yang Sakit/Izin Hari Ini!

+

+ Terdapat {{ $abandonedTasks->count() }} Penugasan (Belum Mulai / Dalam Proses) yang anggotanya hari ini berstatus Sakit / Izin. Segera cek dan sesuaikan formasi tim berikut: +

+
+ @foreach($abandonedTasks as $task) +
+ {{ \Illuminate\Support\Str::limit($task->jenis_pekerjaan, 20) }} + {{ \Illuminate\Support\Str::limit($task->alamat_lokasi, 25) }} + +
+ @endforeach +
+
+
+ @endif + + +
+
+
+
Total
+
{{ $totalPenugasan ?? 0 }}
+
Semua penugasan
+
+
+
+
Belum Mulai
+
{{ $belumMulai ?? 0 }}
+
Menunggu eksekusi
+
+
+
+
Dalam Proses
+
{{ $dalamProses ?? 0 }}
+
Sedang dikerjakan
+
+
+
+
Selesai
+
{{ $selesai ?? 0 }}
+
Pekerjaan rampung
+
+
+
+
Dibatalkan
+
{{ $dibatalkan ?? 0 }}
+
Penugasan dibatalkan
+
+
+
+
Garansi Aktif
+
{{ $garansiAktif ?? 0 }}
+
Dalam periode garansi
+
+
+ + +
+ + +
+
+ + +
+ +
+ + S/D + +
+
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+
+
+ + +
+
+
+ + Daftar Penugasan +
+
+ {{ isset($penugasan) ? $penugasan->total() : 0 }} data ditemukan +
+
+ +
+ + + + + + + + + + + + + @forelse($penugasan ?? [] as $index => $item) + + + + + + + + + + + @empty + + + + @endforelse + +
#TeknisiSuratTanggalStatus PekerjaanAksi
+ {{ str_pad($penugasan->firstItem() + $index, 2, '0', STR_PAD_LEFT) }} + +
+ @foreach($item->timTeknisi as $tt) + + {{ $tt->teknisi->nama ?? 'N/A' }} + + @endforeach +
+
+ @if($item->foto_surat) + Surat + @else + N/A + @endif + +
{{ \Carbon\Carbon::parse($item->tanggal_diberikan)->format('d/m/Y') }}
+
+ @php + $bMap = [ + 'belum_mulai' => ['class'=>'pug-badge-muted', 'icon'=>'fa-clock', 'label'=>'Belum Mulai'], + 'dalam_proses'=> ['class'=>'pug-badge-violet', 'icon'=>'fa-spinner', 'label'=>'Dalam Proses'], + 'selesai' => ['class'=>'pug-badge-green', 'icon'=>'fa-check-circle', 'label'=>'Selesai'], + 'dibatalkan' => ['class'=>'pug-badge-rose', 'icon'=>'fa-times-circle', 'label'=>'Dibatalkan'], + ]; + $bCfg = $bMap[$item->status_pekerjaan] ?? $bMap['belum_mulai']; + @endphp + + {{ $bCfg['label'] }} + + +
+ + + +
+
+
+
+
Belum ada penugasan
+
Klik "Tambah Penugasan" untuk membuat penugasan baru
+
+
+
+ + @if(isset($penugasan) && $penugasan->hasPages()) +
{{ $penugasan->links() }}
+ @endif +
+ +
+
+
+ + +
+
+
+
Tambah Penugasan Baru
+ +
+ +
+ +

Metode Foto: Silakan unggah foto daftar tugas/surat tugas dari kantor. Teknisi akan melihat foto ini di aplikasi mereka.

+
+ +
+ @csrf + + + +
+ +
+ + +
+
+
+ +
+ +
+ @foreach($teknisiList ?? [] as $teknisi) +
+ {{ $teknisi->nama }} + {{ $teknisi->spesialisasi }} +
+ @endforeach +
Tidak ada hasil
+
+
+ + +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+ +
+ +

Klik untuk unggah denah lokasi atau surat tugas

+ Format: JPG, PNG, WEBP (Boleh dikosongkan) +
+ +
+
+ + +
+ + +
+
+ + +
+
+
+ + +
+
+
+
Detail Penugasan
+ +
+
+
+
+ + +
+ Preview + +
+ + + \ No newline at end of file diff --git a/samooapk/resources/views/Admin/KelolaPekerjaan/ProgresKerja.blade.php b/samooapk/resources/views/Admin/KelolaPekerjaan/ProgresKerja.blade.php new file mode 100644 index 0000000..eea5320 --- /dev/null +++ b/samooapk/resources/views/Admin/KelolaPekerjaan/ProgresKerja.blade.php @@ -0,0 +1,314 @@ + +@push('styles') + + + +@endpush + +
+
+ +
+
+
+ +
+
+

Monitoring Progres Kerja

+

Pantau real-time perkembangan tugas teknisi di lapangan.

+
+
+
+ + +
+
+
+
TOTAL TUGAS
+
{{ $statistics['total'] ?? 0 }}
+
+
+
+
DALAM PROGRES
+
{{ $statistics['dalam_progres'] ?? 0 }}
+
+
+
+
SELESAI
+
{{ $statistics['selesai'] ?? 0 }}
+
+
+
+
BELUM MULAI
+
{{ $statistics['belum_mulai'] ?? 0 }}
+
+
+ +
+ +
+
+ +
+ + +
+
+ +
+ + s/d + +
+
+ +
+
+ + +
+
+
+ DAFTAR PROGRES KERJA +
+
+ Menampilkan {{ count($progresKerja) }} data terbaru +
+
+ +
+ + + + + + + + + + + + + @forelse($progresKerja as $index => $progres) + + + + + + + + + @empty + + + + @endforelse + +
#Teknisi / TimFotoPekerjaan & LokasiStatusAksi
+ {{ str_pad($progresKerja->firstItem() + $index, 2, '0', STR_PAD_LEFT) }} + +
+ @foreach($progres->timTeknisi as $member) + + {{ $member->teknisi->nama }} + + @endforeach +
+
+ @php + $fotoTampil = $progres->foto_sesudah ?? $progres->foto_sebelum ?? $progres->foto_surat; + @endphp + @if($fotoTampil) + + @else +
+ +
+ @endif +
+
{{ $progres->label_jenis_pekerjaan }}
+
{{ Str::limit($progres->alamat_lokasi, 40) }}
+
+ @switch($progres->status_pekerjaan) + @case('belum_mulai') Belum Mulai @break + @case('dalam_proses') Progres @break + @case('selesai') Selesai @break + @endswitch +
{{ \Carbon\Carbon::parse($progres->updated_at)->format('d/m H:i') }}
+
+
+ +
+
+
+
+
Data Tidak Ditemukan
+
Gunakan filter untuk mencari data progres lainnya.
+
+
+
+ + @if($progresKerja->hasPages()) +
+ {{ $progresKerja->links() }} +
+ @endif +
+
+
+
+ + +
+
+
+
+ DETAIL PROGRES KERJA +
+ +
+
+ +
+
+

Mengambil data...

+
+
+
+
+ + +
+ + +
+ +@push('scripts') + + +@endpush +
\ No newline at end of file diff --git a/samooapk/resources/views/Admin/KelolaTeknisi/Absensi.blade.php b/samooapk/resources/views/Admin/KelolaTeknisi/Absensi.blade.php new file mode 100644 index 0000000..b148148 --- /dev/null +++ b/samooapk/resources/views/Admin/KelolaTeknisi/Absensi.blade.php @@ -0,0 +1,407 @@ + +@push('styles') + + + +@endpush + +
+
+ {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+

Monitoring Absensi

+

Kelola & monitor kehadiran teknisi lapangan

+
+
+
+ + {{-- ── STATISTICS CARDS ── --}} +
+
+
+
TOTAL ABSENSI
+
{{ $counts['total'] ?? 0 }}
+
Semua teknisi terdaftar
+
+
+
+
HADIR
+
{{ $counts['hadir'] ?? 0 }}
+
Teknisi yang sudah masuk
+
+
+
+
IZIN / SAKIT
+
{{ $counts['izin'] ?? 0 }}
+
Izin berhalangan kerja
+
+
+ + +
+ +
+
+ +
+ + s/d + +
+
+ +
+ +
+ + +
+
+ +
+ +
+
+
+ + {{-- ── PANEL TABLE ── --}} +
+
+
+ DAFTAR LIST ABSENSI +
+
+ Menampilkan {{ $absensis->total() }} data absensi +
+
+ +
+ + + + + + + + + + + + + + + @forelse($absensis as $index => $abs) + + + + + + + + + + + @empty + + + + @endforelse + +
#TeknisiTanggalJam MasukJam KeluarDurasiStatusAksi
+ {{ str_pad($absensis->firstItem() + $index, 2, '0', STR_PAD_LEFT) }} + +
+
+ {{ strtoupper(substr($abs->teknisi->nama ?? 'T', 0, 1)) }} +
+
+
{{ $abs->teknisi->nama ?? 'Unknown' }}
+
{{ $abs->teknisi->email ?? '—' }}
+
+
+
+
{{ \Carbon\Carbon::parse($abs->tanggal)->format('d/m/Y') }}
+
{{ \Carbon\Carbon::parse($abs->tanggal)->isoFormat('dddd') }}
+
+ @if($abs->jam_masuk) +
{{ \Carbon\Carbon::parse($abs->jam_masuk)->format('H:i') }}
+ @php + $kat = $abs->kategori_kerja; + $badgeClass = $kat == 'Kerja Urgent' ? 'badge-urgent' : 'badge-normal'; + $icon = $kat == 'Kerja Urgent' ? 'exclamation-triangle' : 'sun'; + @endphp +
+ {{ $kat }} +
+ @else + + @endif +
+ {{ $abs->jam_keluar ? \Carbon\Carbon::parse($abs->jam_keluar)->format('H:i') : '—' }} + + {{ $abs->durasi_kerja_formatted ?? '—' }} + + @php + $s = strtolower($abs->status); + $badgeClass = 'pug-badge-green'; + $icon = 'check-circle'; + if($s == 'izin' || $s == 'sakit') { $badgeClass = 'pug-badge-violet'; $icon = 'info-circle'; } + @endphp + + {{ ucfirst($abs->status) }} + + +
+ @if(!empty($abs->latitude) && !empty($abs->longitude) && $abs->latitude !== '0' && $abs->longitude !== '0') + + + + @endif + + +
+
+
+
+
Data Absensi Tidak Ditemukan
+
Gunakan filter untuk mencari data di tanggal lain.
+
+
+
+ @if($absensis->hasPages()) +
+ {{ $absensis->appends(request()->query())->links() }} +
+ @endif +
+
+
+ + +
+
+
+
DETAIL ABSENSI TEKNISI
+ +
+
+
+
+ + +
+
+
+
EDIT DATA ABSENSI
+ +
+
+ @csrf @method('PUT') + +
+
+ +

+

+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
\ No newline at end of file diff --git a/samooapk/resources/views/Admin/KelolaTeknisi/AkunTeknisi.blade.php b/samooapk/resources/views/Admin/KelolaTeknisi/AkunTeknisi.blade.php new file mode 100644 index 0000000..c34360d --- /dev/null +++ b/samooapk/resources/views/Admin/KelolaTeknisi/AkunTeknisi.blade.php @@ -0,0 +1,510 @@ + + +@push('styles') + + + +@endpush + +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+

Kelola Akun Teknisi

+

Manajemen akun & akses aplikasi teknisi lapangan

+
+
+ +
+ + {{-- ── STATISTICS CARDS ── --}} +
+
+
+
TOTAL AKUN
+
{{ $akunTeknisis->count() }}
+
Semua akun terdaftar
+
+
+
+
AKUN AKTIF
+
{{ $akunTeknisis->where('status','aktif')->count() }}
+
Memiliki akses aplikasi
+
+
+
+
NONAKTIF
+
{{ $akunTeknisis->where('status','tidak_aktif')->count() }}
+
Akses akun dicabut
+
+
+ + {{-- ── ALERT ── --}} + + + {{-- ── TOOLBAR ── --}} +
+
+
+ +
+ + +
+
+ +
+
+ + {{-- ── PANEL TABLE ── --}} +
+
+
+ DAFTAR AKUN TEKNISI +
+
+ Menampilkan {{ $akunTeknisis->count() }} akun terdaftar +
+
+ +
+ + + + + + + + + + + + @forelse($akunTeknisis as $index => $akun) + + + + + + + + @empty + + + + @endforelse + + + + +
#Nama TeknisiUsernameStatus AkunAksi
+ {{ str_pad($index + 1, 2, '0', STR_PAD_LEFT) }} + +
+
+ {{ strtoupper(substr($akun->teknisi->nama ?? 'T', 0, 1)) }} +
+
+
{{ $akun->teknisi->nama ?? '—' }}
+
Teknisi Lapangan
+
+
+
+ + {{ $akun->username }} + + +
+ @if($akun->status == 'aktif') + + Aktif + + @else + + Nonaktif + + @endif + +
+
+
+ + + +
+
+
+
+
Belum Ada Akun Teknisi
+
Silakan tambahkan akun untuk akses aplikasi teknisi.
+
+
+
+
+ +{{-- ── MODAL DETAIL AKUN ── --}} +
+
+
+
DETAIL AKUN TEKNISI
+ +
+
+
+
+

+
+
+ +
+
+ +
+ + +
+
+ +
+ +
+ + +
+

* Gunakan fitur 'Edit' untuk mereset password jika lupa.

+
+ +
+ +
+ +
+
+
ID Akun
+
+
+
+
ID Teknisi
+
+
+
+
Terdaftar Sejak
+
+
+
+
+
+
+ +
+
+ +
+
+ +{{-- ── MODAL TAMBAH ── --}} +
+
+
+
TAMBAH AKUN TEKNISI
+ +
+
+ @csrf +
+
+ + + +
+
+ + + +
+
+ +
+ + +
+ +
+
+ + +
+
+ +
+
+
+ +{{-- ── MODAL EDIT ── --}} +
+
+
+
EDIT AKUN TEKNISI
+ +
+
+ @csrf @method('PUT') + + +
+
+ + +
+
+ + + +
+
+ +
+ + +
+
+
+ + +
+
+ +
+
+
+ + + +
\ No newline at end of file diff --git a/samooapk/resources/views/Admin/KelolaTeknisi/Teknisi.blade.php b/samooapk/resources/views/Admin/KelolaTeknisi/Teknisi.blade.php new file mode 100644 index 0000000..5c19064 --- /dev/null +++ b/samooapk/resources/views/Admin/KelolaTeknisi/Teknisi.blade.php @@ -0,0 +1,456 @@ + + +@push('styles') + + + +@endpush + +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+

Data Teknisi

+

Kelola & monitor profil lengkap teknisi lapangan

+
+
+ +
+ + {{-- ── STATISTICS CARDS ── --}} +
+
+
+
TOTAL TEKNISI
+
0
+
Semua teknisi terdaftar
+
+
+
+
TEKNISI AKTIF
+
0
+
Sedang aktif bertugas
+
+
+
+
TIDAK AKTIF
+
0
+
Tidak sedang bertugas
+
+
+ + {{-- ── TOOLBAR (SEARCH) ── --}} +
+
+
+ +
+ + +
+
+
+ +
+
+
+ + {{-- ── PANEL TABLE ── --}} +
+
+
+ DAFTAR INFORMASI TEKNISI +
+
Memuat data…
+
+ +
+ + + + + + + + + + + + + + + + +
#Nama TeknisiNo. TeleponTgl MasukStatusAksi
+
+
+
Memuat Data...
+
+
+
+ +
+
+ + Menampilkan 00 dari 0 teknisi +
+
+
+ +
+
+ +{{-- ── MODAL DETAIL TEKNISI ── --}} +
+
+
+
Detail Profil Teknisi
+ +
+
+
+
+

+

+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ +
+
+
+ +
+
+
+
+
+ +
+
+ +{{-- ── MODAL TAMBAH / EDIT ── --}} + + + + +
\ No newline at end of file diff --git a/samooapk/resources/views/Admin/Laporan.blade.php b/samooapk/resources/views/Admin/Laporan.blade.php new file mode 100644 index 0000000..04c7a2d --- /dev/null +++ b/samooapk/resources/views/Admin/Laporan.blade.php @@ -0,0 +1,257 @@ + +@push('styles') + + +@endpush + +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+

Laporan Sistem

+

Statistik dan rincian operasional PDAM • {{ date('F Y') }}

+
+
+
+ + {{-- ── TAB NAVIGATION ── --}} + + + {{-- ── STATS ── --}} +
+
+
+
TOTAL TENAGA KERJA
+
{{ $statistics['total_teknisi'] ?? 0 }}
+
{{ $statistics['teknisi_aktif'] ?? 0 }} aktif • {{ $statistics['teknisi_nonaktif'] ?? 0 }} nonaktif
+
+
+
+
PEKERJAAN BULAN INI
+
{{ $statistics['selesai'] ?? 0 }}
+
{{ $statistics['progress'] ?? 0 }} proses • {{ $statistics['pending'] ?? 0 }} pending
+
+
+
+
KASBON BELUM LUNAS
+
Rp {{ number_format($statistics['total_belum_lunas'] ?? 0, 0, ',', '.') }}
+
Dari {{ $statistics['total_kasbon'] ?? 0 }} total kasbon
+
+
+
+
KEHADIRAN BULAN INI
+
{{ $statistics['hadir'] ?? 0 }}
+
{{ $statistics['izin'] ?? 0 }} izin • {{ $statistics['sakit'] ?? 0 }} sakit • {{ $statistics['alpha'] ?? 0 }} alpha
+
+
+ + {{-- ── TABLES ── --}} + + +
+
+
+ + Kasbon Terbaru +
+ +
+
+ + + + + + + + + + + + @forelse($recentKasbon as $item) + + + + + + + + @empty + + + + @endforelse + +
TeknisiTanggalJumlahStatusKeperluan
+
+
{{ strtoupper(substr($item->nama_teknisi ?? '?', 0, 2)) }}
+
{{ $item->nama_teknisi ?? '-' }}
+
+
{{ \Carbon\Carbon::parse($item->tanggal_kasbon)->format('d M Y') }}
Rp {{ number_format($item->jumlah_kasbon, 0, ',', '.') }}
+ + {{ strtoupper($item->status) }} + + {{ $item->keperluan ?? '-' }}
+
+
Tidak ada data kasbon
+
+
+
+
+ +
+ +
+
+
+ + Absensi Terbaru +
+
+ Semua +
+
+
+ + + + + + + + + + @forelse($recentAbsensi as $item) + + + + + + @empty + + @endforelse + +
TeknisiTanggalStatus
+
+
{{ strtoupper(substr($item->nama_teknisi ?? '?', 0, 2)) }}
+
{{ $item->nama_teknisi ?? '-' }}
+
+
{{ \Carbon\Carbon::parse($item->tanggal)->format('d M Y') }}
+ @php + $st = strtolower($item->status); + $bc = 'pug-badge-amber'; $ic = 'fa-clock'; + if($st == 'hadir') { $bc = 'pug-badge-green'; $ic = 'fa-check-circle'; } + if(in_array($st, ['alpha','sakit'])) { $bc = 'pug-badge-rose'; $ic = 'fa-times-circle'; } + if($st == 'izin') { $bc = 'pug-badge-violet'; $ic = 'fa-info-circle'; } + @endphp + + {{ strtoupper($item->status) }} + +
Tidak ada data
+
+
+ + +
+
+
+ + Pekerjaan Terbaru +
+
+ Semua +
+
+
+ + + + + + + + + + @forelse($recentPekerjaan as $item) + + + + + + @empty + + @endforelse + +
PekerjaanTeknisiStatus
+
+
{{ $item->jenis_pekerjaan ?? '-' }}
+
#{{ $item->id_penugasan }}
+
+
+
+
{{ strtoupper(substr($item->nama_teknisi ?? '?', 0, 2)) }}
+
{{ $item->nama_teknisi ?? '-' }}
+
+
+ @php + $sp = strtolower($item->status_pekerjaan ?? 'pending'); + $bcp = 'pug-badge-muted'; $icp = 'fa-clock'; + if($sp == 'selesai') { $bcp = 'pug-badge-green'; $icp = 'fa-check-circle'; } + if($sp == 'proses' || $sp == 'dalam_proses') { $bcp = 'pug-badge-violet'; $icp = 'fa-spinner'; } + @endphp + + {{ strtoupper(str_replace('_', ' ', $item->status_pekerjaan ?? '-')) }} + +
Tidak ada data
+
+
+
+ +
+
+
\ No newline at end of file diff --git a/samooapk/resources/views/Admin/Laporan/absensi.blade.php b/samooapk/resources/views/Admin/Laporan/absensi.blade.php new file mode 100644 index 0000000..630b628 --- /dev/null +++ b/samooapk/resources/views/Admin/Laporan/absensi.blade.php @@ -0,0 +1,196 @@ + +@push('styles') + + +@endpush + +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+

Laporan Kehadiran

+

Rekapitulasi kehadiran harian teknisi PDAM • {{ date('F Y') }}

+
+
+ + Cetak Laporan + +
+ + {{-- ── TAB NAVIGATION ── --}} + + + {{-- ── STATS ── --}} +
+
+
+
HADIR BULAN INI
+
{{ $statsAbsensi['hadir'] ?? 0 }}
+
Kehadiran tercatat
+
+
+
+
IZIN
+
{{ $statsAbsensi['izin'] ?? 0 }}
+
Permohonan izin
+
+
+
+
SAKIT
+
{{ $statsAbsensi['sakit'] ?? 0 }}
+
Keterangan sakit
+
+
+
+
ALPHA
+
{{ $statsAbsensi['alpha'] ?? 0 }}
+
Tanpa keterangan
+
+
+ + {{-- ── TOOLBAR FILTER ── --}} +
+
+ +
+ +
+ + +
+
+ +
+ + @if(request()->hasAny(['tanggal'])) + + Reset + + @endif +
+
+
+ + {{-- ── TABLE PANEL ── --}} +
+
+
+ + Data Laporan Kehadiran +
+
+ {{ $data->total() }} data ditemukan +
+
+ +
+ + + + + + + + + + + @forelse($data as $item) + + + + + + + @empty + + + + @endforelse + +
TeknisiTanggalStatus KehadiranKeterangan
+
+
{{ strtoupper(substr($item->nama_teknisi ?? '?', 0, 2)) }}
+
{{ $item->nama_teknisi ?? '-' }}
+
+
{{ \Carbon\Carbon::parse($item->tanggal)->format('d M Y') }}
+ @php + $st = strtolower($item->status); + $bc = 'pug-badge-amber'; $ic = 'fa-clock'; + if($st == 'hadir') { $bc = 'pug-badge-green'; $ic = 'fa-check-circle'; } + if(in_array($st, ['alpha','sakit'])) { $bc = 'pug-badge-rose'; $ic = 'fa-times-circle'; } + if($st == 'izin') { $bc = 'pug-badge-violet'; $ic = 'fa-info-circle'; } + @endphp + + {{ strtoupper($item->status) }} + + + {{ $item->keterangan ?? '-' }} + @if(!empty($item->latitude) && !empty($item->longitude)) + + Cek Lokasi + + @endif +
+
+
+
Tidak ada data kehadiran
+
Ubah filter pencarian untuk melihat data lain.
+
+
+
+ + @if($data->hasPages()) +
+ {{ $data->links() }} +
+ @endif +
+ +
+
+
diff --git a/samooapk/resources/views/Admin/Laporan/data_teknisi.blade.php b/samooapk/resources/views/Admin/Laporan/data_teknisi.blade.php new file mode 100644 index 0000000..6d1115c --- /dev/null +++ b/samooapk/resources/views/Admin/Laporan/data_teknisi.blade.php @@ -0,0 +1,194 @@ + +@push('styles') + + +@endpush + +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+

Laporan Data Teknisi

+

Daftar lengkap seluruh teknisi beserta status aktif • {{ date('F Y') }}

+
+
+ + Cetak Laporan + +
+ + {{-- ── TAB NAVIGATION ── --}} + + + {{-- ── STATS ── --}} +
+
+
+
TOTAL TEKNISI
+
{{ $statsTeknisi['total'] ?? 0 }}
+
Seluruh teknisi terdaftar
+
+
+
+
TEKNISI AKTIF
+
{{ $statsTeknisi['aktif'] ?? 0 }}
+
Status akun aktif
+
+
+
+
NONAKTIF
+
{{ $statsTeknisi['nonaktif'] ?? 0 }}
+
Status akun tidak aktif
+
+
+ + {{-- ── TOOLBAR FILTER ── --}} +
+
+ +
+ +
+ + +
+
+ +
+ + +
+ +
+ + @if(request()->hasAny(['search', 'status'])) + + Reset + + @endif +
+
+
+ + {{-- ── TABLE PANEL ── --}} +
+
+
+ + Data Lengkap Teknisi +
+
+ {{ $data->total() }} data ditemukan +
+
+ +
+ + + + + + + + + + + @forelse($data as $item) + + + + + + + @empty + + + + @endforelse + +
TeknisiNo TelpTgl MasukStatus
+
+
{{ strtoupper(substr($item->nama ?? '?', 0, 2)) }}
+
+
{{ $item->nama ?? '-' }}
+
{{ $item->email ?? '-' }}
+
+
+
+
+ {{ $item->no_telephone ?? '-' }} +
+
+
{{ $item->tanggal_masuk ? \Carbon\Carbon::parse($item->tanggal_masuk)->format('d M Y') : '-' }}
+
+ + {{ strtoupper(str_replace('_', ' ', $item->status ?? '-')) }} + +
+
+
+
Tidak ada data teknisi
+
Ubah filter pencarian untuk melihat data lain.
+
+
+
+ + @if($data->hasPages()) +
+ {{ $data->links() }} +
+ @endif +
+ +
+
+
diff --git a/samooapk/resources/views/Admin/Laporan/kasbon.blade.php b/samooapk/resources/views/Admin/Laporan/kasbon.blade.php new file mode 100644 index 0000000..963acbd --- /dev/null +++ b/samooapk/resources/views/Admin/Laporan/kasbon.blade.php @@ -0,0 +1,206 @@ + +@push('styles') + + +@endpush + +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+

Laporan Kasbon

+

Rincian pinjaman dan status pelunasan teknisi • {{ date('F Y') }}

+
+
+ + Cetak Laporan + +
+ + {{-- ── TAB NAVIGATION ── --}} + + + {{-- ── STATS ── --}} +
+
+
+
TOTAL KASBON
+
{{ $statsKasbon['total'] ?? 0 }}
+
Rp {{ number_format($statsKasbon['total_nominal'] ?? 0, 0, ',', '.') }}
+
+
+
+
LUNAS
+
{{ $statsKasbon['lunas'] ?? 0 }}
+
Kasbon yang sudah dibayar
+
+
+
+
BELUM LUNAS
+
{{ $statsKasbon['belum_lunas'] ?? 0 }}
+
Menunggu pelunasan
+
+
+
+
NOMINAL BELUM LUNAS
+
Rp {{ number_format($statsKasbon['total_belum_lunas'] ?? 0, 0, ',', '.') }}
+
Total tagihan tertunggak
+
+
+ + {{-- ── TOOLBAR FILTER ── --}} +
+
+ +
+ +
+ + +
+
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+ + @if(request()->hasAny(['search', 'status', 'id_teknisi'])) + + Reset + + @endif +
+
+
+ + {{-- ── TABLE PANEL ── --}} +
+
+
+ + Data Laporan Kasbon +
+
+ {{ $data->total() }} data ditemukan +
+
+ +
+ + + + + + + + + + + + @forelse($data as $item) + + + + + + + + @empty + + + + @endforelse + +
TeknisiTanggal PinjamJumlah KasbonStatusKeperluan
+
+
{{ strtoupper(substr($item->nama_teknisi ?? '?', 0, 2)) }}
+
{{ $item->nama_teknisi ?? '-' }}
+
+
{{ \Carbon\Carbon::parse($item->tanggal_kasbon)->format('d M Y') }}
Rp {{ number_format($item->jumlah_kasbon, 0, ',', '.') }}
+ + {{ strtoupper($item->status) }} + + {{ $item->keperluan ?? '-' }}
+
+
+
Tidak ada data kasbon
+
Ubah filter pencarian untuk melihat data lain.
+
+
+
+ + @if($data->hasPages()) +
+ {{ $data->links() }} +
+ @endif +
+ +
+
+
diff --git a/samooapk/resources/views/Admin/Laporan/pekerjaan.blade.php b/samooapk/resources/views/Admin/Laporan/pekerjaan.blade.php new file mode 100644 index 0000000..c69a38e --- /dev/null +++ b/samooapk/resources/views/Admin/Laporan/pekerjaan.blade.php @@ -0,0 +1,220 @@ + +@push('styles') + + +@endpush + +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+

Laporan Pekerjaan

+

Daftar penugasan dan status pengerjaan teknisi • {{ date('F Y') }}

+
+
+ + Cetak Laporan + +
+ + {{-- ── TAB NAVIGATION ── --}} + + + {{-- ── STATS ── --}} +
+
+
+
TOTAL PENUGASAN
+
{{ $statsPekerjaan['total'] ?? 0 }}
+
Seluruh penugasan
+
+
+
+
SELESAI
+
{{ $statsPekerjaan['selesai'] ?? 0 }}
+
Pekerjaan tuntas
+
+
+
+
DALAM PROSES
+
{{ $statsPekerjaan['proses'] ?? 0 }}
+
Sedang dikerjakan
+
+
+
+
PENDING
+
{{ $statsPekerjaan['pending'] ?? 0 }}
+
Menunggu dikerjakan
+
+
+ + {{-- ── TOOLBAR FILTER ── --}} +
+
+ +
+ +
+ + +
+
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+ + @if(request()->hasAny(['tanggal', 'status', 'id_teknisi'])) + + Reset + + @endif +
+
+
+ + {{-- ── TABLE PANEL ── --}} +
+
+
+ + Data Laporan Pekerjaan +
+
+ {{ $data->total() }} data ditemukan +
+
+ +
+ + + + + + + + + + + + @forelse($data as $item) + + + + + + + + @empty + + + + @endforelse + +
PekerjaanTeknisiLokasiStatusTgl Selesai
+
+
{{ $item->jenis_pekerjaan ?? '-' }}
+
#{{ $item->id_penugasan }}
+
+
+
+
{{ strtoupper(substr($item->nama_teknisi ?? '?', 0, 2)) }}
+
{{ $item->nama_teknisi ?? '-' }}
+
+
+
{{ $item->catatan_admin ?? '-' }}
+
+ @php + $sp = strtolower($item->status_pekerjaan ?? 'pending'); + $bcp = 'pug-badge-muted'; $icp = 'fa-clock'; + if($sp == 'selesai') { $bcp = 'pug-badge-green'; $icp = 'fa-check-circle'; } + if($sp == 'proses' || $sp == 'dalam_proses') { $bcp = 'pug-badge-violet'; $icp = 'fa-spinner'; } + @endphp + + {{ strtoupper(str_replace('_', ' ', $item->status_pekerjaan ?? '-')) }} + +
{{ $item->tanggal_diselesaikan ? \Carbon\Carbon::parse($item->tanggal_diselesaikan)->format('d M Y') : '-' }}
+
+
+
Tidak ada data pekerjaan
+
Ubah filter pencarian untuk melihat data lain.
+
+
+
+ + @if($data->hasPages()) +
+ {{ $data->links() }} +
+ @endif +
+ +
+
+
diff --git a/samooapk/resources/views/Admin/Laporan/penggajian.blade.php b/samooapk/resources/views/Admin/Laporan/penggajian.blade.php new file mode 100644 index 0000000..18ab488 --- /dev/null +++ b/samooapk/resources/views/Admin/Laporan/penggajian.blade.php @@ -0,0 +1,204 @@ + +@push('styles') + + +@endpush + +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+

Laporan Penggajian

+

Rincian penggajian dan status pembayaran teknisi • {{ date('F Y') }}

+
+
+ + Cetak Laporan + +
+ + {{-- ── TAB NAVIGATION ── --}} + + + {{-- ── STATS ── --}} +
+
+
+
TOTAL PENGGAJIAN
+
{{ $statsPenggajian['total'] ?? 0 }}
+
Keseluruhan data penggajian
+
+
+
+
LUNAS
+
{{ $statsPenggajian['lunas'] ?? 0 }}
+
Penggajian telah dibayar
+
+
+
+
BELUM DIBAYAR
+
{{ $statsPenggajian['belum'] ?? 0 }}
+
Menunggu pembayaran
+
+
+ + {{-- ── TOOLBAR FILTER ── --}} +
+
+ +
+ +
+ + +
+
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+ + @if(request()->hasAny(['tanggal', 'status', 'id_teknisi'])) + + Reset + + @endif +
+
+
+ + {{-- ── TABLE PANEL ── --}} +
+
+
+ + Data Laporan Penggajian +
+
+ {{ $data->total() }} data ditemukan +
+
+ +
+ + + + + + + + + + + @forelse($data as $item) + + + + + + + @empty + + + + @endforelse + +
TeknisiPeriodeGaji BersihStatus Pembayaran
+
+
{{ strtoupper(substr($item->nama_teknisi ?? '?', 0, 2)) }}
+
{{ $item->nama_teknisi ?? '-' }}
+
+
+ @php + $bulan = [1=>'Januari',2=>'Februari',3=>'Maret',4=>'April',5=>'Mei',6=>'Juni',7=>'Juli',8=>'Agustus',9=>'September',10=>'Oktober',11=>'November',12=>'Desember']; + $namaBulan = $bulan[(int)$item->periode_bulan] ?? '-'; + @endphp +
{{ $namaBulan }} {{ $item->periode_tahun }}
+
Rp {{ number_format($item->gaji_bersih, 0, ',', '.') }}
+ + {{ strtoupper(str_replace('_', ' ', $item->status_pembayaran)) }} + +
+
+
+
Tidak ada data penggajian
+
Ubah filter pencarian untuk melihat data lain.
+
+
+
+ + @if($data->hasPages()) +
+ {{ $data->links() }} +
+ @endif +
+ +
+
+
diff --git a/samooapk/resources/views/Admin/Laporan/print.blade.php b/samooapk/resources/views/Admin/Laporan/print.blade.php new file mode 100644 index 0000000..ba9ff5a --- /dev/null +++ b/samooapk/resources/views/Admin/Laporan/print.blade.php @@ -0,0 +1,234 @@ + + + + + + Cetak Laporan - {{ strtoupper($title ?? 'Laporan') }} + + + + +
+ + +
+ +
+ Logo PDAM +

PERUSAHAAN DAERAH AIR MINUM

+

(PDAM)

+

Jl. Kendedes No.8, Gianyar, Kec. Gianyar, Kabupaten Gianyar, Bali 80511

+

Telp: +62 823-3152-7309 | Email: tekleperumda@gmail.com

+
+ +
+ LAPORAN {{ $title ?? 'DATA' }} +
+ +
+ @if(!empty($filters['tanggal_dari']) || !empty($filters['tanggal_sampai'])) + Periode: + {{ !empty($filters['tanggal_dari']) ? \Carbon\Carbon::parse($filters['tanggal_dari'])->format('d M Y') : 'Awal' }} + s/d + {{ !empty($filters['tanggal_sampai']) ? \Carbon\Carbon::parse($filters['tanggal_sampai'])->format('d M Y') : 'Sekarang' }}
+ @else + Periode: Semua Waktu
+ @endif + + @if(!empty($filters['status'])) + Status: {{ strtoupper(str_replace('_', ' ', $filters['status'])) }}
+ @endif + + @if(!empty($filters['search'])) + Pencarian: "{{ $filters['search'] }}"
+ @endif +
+ + + + + + @foreach($columns as $col) + + @endforeach + + + + @forelse($data as $index => $item) + + + + @if($type == 'kasbon') + + + + + + + @elseif($type == 'teknisi') + + + + + + + + + + @elseif($type == 'absensi') + + + + + + @elseif($type == 'pekerjaan') + + + + + + + + @elseif($type == 'penggajian') + + + + + + @elseif($type == 'data_teknisi') + + + + + + @endif + + @empty + + + + @endforelse + +
No{{ $col }}
{{ $index + 1 }}{{ $item->nama_teknisi ?? '-' }}{{ \Carbon\Carbon::parse($item->tanggal_kasbon)->format('d/m/Y') }}Rp {{ number_format($item->jumlah_kasbon, 0, ',', '.') }}{{ strtoupper($item->status) }}{{ $item->keperluan ?? '-' }}{{ $item->nama }}{{ strtoupper($item->status) }}{{ $item->hadir }}{{ $item->izin }}{{ $item->sakit ?? 0 }}{{ $item->alpha }}{{ $item->total_absensi }} + {{ $item->total_absensi > 0 ? round(($item->hadir / $item->total_absensi) * 100, 1) : 0 }}% + {{ $item->nama_teknisi ?? '-' }}{{ \Carbon\Carbon::parse($item->tanggal)->format('d/m/Y') }}{{ strtoupper($item->status) }}{{ $item->keterangan ?? '-' }}{{ $item->id_penugasan }}{{ $item->jenis_pekerjaan ?? '-' }}{{ $item->nama_teknisi ?? '-' }}{{ strtoupper(str_replace('_', ' ', $item->status_pekerjaan)) }}{{ $item->tanggal_mulai ? \Carbon\Carbon::parse($item->tanggal_mulai)->format('d/m/Y') : '-' }}{{ $item->tanggal_diselesaikan ? \Carbon\Carbon::parse($item->tanggal_diselesaikan)->format('d/m/Y') : '-' }}{{ $item->nama_teknisi ?? '-' }} + @php + $bulan = [1=>'Januari',2=>'Februari',3=>'Maret',4=>'April',5=>'Mei',6=>'Juni',7=>'Juli',8=>'Agustus',9=>'September',10=>'Oktober',11=>'November',12=>'Desember']; + $namaBulan = $bulan[(int)$item->periode_bulan] ?? '-'; + @endphp + {{ $namaBulan }} {{ $item->periode_tahun }} + Rp {{ number_format($item->gaji_bersih, 0, ',', '.') }}{{ strtoupper(str_replace('_', ' ', $item->status_pembayaran)) }}{{ $item->nama ?? '-' }}{{ $item->email ?? '-' }}{{ $item->no_telephone ?? '-' }}{{ $item->tanggal_masuk ? \Carbon\Carbon::parse($item->tanggal_masuk)->format('d/m/Y') : '-' }}{{ strtoupper(str_replace('_', ' ', $item->status ?? '-')) }}
Tidak ada data yang sesuai dengan filter.
+ +
+
+ +
+
+

Gianyar, {{ \Carbon\Carbon::now()->format('d F Y') }}

+

Mandor Lapangan

+
+

(Kusaini)

+
+
+ + + diff --git a/samooapk/resources/views/Admin/Laporan/teknisi.blade.php b/samooapk/resources/views/Admin/Laporan/teknisi.blade.php new file mode 100644 index 0000000..3f57cea --- /dev/null +++ b/samooapk/resources/views/Admin/Laporan/teknisi.blade.php @@ -0,0 +1,173 @@ + +@push('styles') + + +@endpush + +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+
+ +
+
+

Performa Teknisi

+

Analisis produktivitas dan persentase kehadiran teknisi • {{ date('F Y') }}

+
+
+ + Cetak Laporan + +
+ + {{-- ── TAB NAVIGATION ── --}} + + + {{-- ── TOOLBAR FILTER ── --}} +
+
+
+ +
+ + +
+
+ +
+ + @if(request()->has('search') && request('search') != '') + + Reset + + @endif +
+
+
+ + {{-- ── TABLE PANEL ── --}} +
+
+
+ + Data Performa Teknisi +
+
+ {{ count($data) }} data ditemukan +
+
+ +
+ + + + + + + + + + + + + + @forelse($data as $item) + + + + + + + + + + @empty + + + + @endforelse + +
TeknisiStatus AkunHadirIzinSakitAlphaTingkat Kehadiran
+
+
{{ strtoupper(substr($item->nama ?? '?', 0, 2)) }}
+
{{ $item->nama ?? '-' }}
+
+
+ + {{ strtoupper($item->status) }} + + {{ $item->hadir }}{{ $item->izin }}{{ $item->sakit ?? 0 }}{{ $item->alpha }} + @php + $persentase = $item->total_absensi > 0 ? round(($item->hadir / $item->total_absensi) * 100, 1) : 0; + $progClass = 'bg-success'; + if ($persentase < 80 && $persentase >= 50) $progClass = 'bg-warning'; + if ($persentase < 50) $progClass = 'bg-danger'; + @endphp +
+ {{ $persentase }}% + {{ $item->total_absensi }} Hari +
+
+
+
+
+
+
+
Tidak ada data teknisi
+
Coba ubah kata kunci pencarian.
+
+
+
+
+ +
+
+
diff --git a/samooapk/resources/views/Admin/dashboard.blade.php b/samooapk/resources/views/Admin/dashboard.blade.php new file mode 100644 index 0000000..79f5d14 --- /dev/null +++ b/samooapk/resources/views/Admin/dashboard.blade.php @@ -0,0 +1,143 @@ + +
+ +
+ + +
+

Dashboard Overview

+
+ + +
+ +
+
+
+

Total Teknisi

+

+ {{ isset($totalTeknisi) ? $totalTeknisi : 0 }} +

+
+
+ +
+
+
+ + +
+
+
+

Teknisi Aktif

+

+ {{ isset($teknisiAktif) ? $teknisiAktif : 0 }} +

+
+
+ +
+
+
+ + +
+
+
+

Total Pekerjaan

+

0

+
+
+ +
+
+
+ + +
+
+
+

Total Laporan

+

0

+
+
+ +
+
+
+
+ + +
+ + + + +
+
+
+
User Information
+
+
+
+ Name:
+ {{ Auth::user()->name }} +
+
+ Email:
+ {{ Auth::user()->email }} +
+
+ Registered:
+ {{ Auth::user()->created_at->format('d M Y') }} +
+
+
+ @csrf + +
+
+
+
+
+
+
+
--> \ No newline at end of file diff --git a/samooapk/resources/views/auth/confirm-password.blade.php b/samooapk/resources/views/auth/confirm-password.blade.php new file mode 100644 index 0000000..3d38186 --- /dev/null +++ b/samooapk/resources/views/auth/confirm-password.blade.php @@ -0,0 +1,27 @@ + +
+ {{ __('This is a secure area of the application. Please confirm your password before continuing.') }} +
+ +
+ @csrf + + +
+ + + + + +
+ +
+ + {{ __('Confirm') }} + +
+
+
diff --git a/samooapk/resources/views/auth/forgot-password.blade.php b/samooapk/resources/views/auth/forgot-password.blade.php new file mode 100644 index 0000000..50b17a8 --- /dev/null +++ b/samooapk/resources/views/auth/forgot-password.blade.php @@ -0,0 +1,332 @@ + + + + + + Lupa Password - SIPDAM + + + + + + + + + +
+ +

Reset Password

+

Kami akan mengirimkan link reset password ke email Anda

+
+
+ + + +
+

Cek Email Anda

+

Setelah submit, cek inbox atau folder spam email Anda untuk link reset password.

+
+
+ + +
+
+ +
+ +
+ + + +
+

Lupa Password?

+

Masukkan email Anda dan kami akan mengirimkan link untuk reset password.

+
+ + @if (session('status')) +
{{ session('status') }}
+ @endif + +
+ @csrf + +
+ + + @error('email') +
{{ $message }}
+ @enderror +
+ + +
+ + + +
+
+ + + \ No newline at end of file diff --git a/samooapk/resources/views/auth/login.blade.php b/samooapk/resources/views/auth/login.blade.php new file mode 100644 index 0000000..ef3ebbc --- /dev/null +++ b/samooapk/resources/views/auth/login.blade.php @@ -0,0 +1,458 @@ + + + + + + Login - SIPDAM + + + + + + + + + +
+ +

Selamat Datang di SIPDAM

+

Sistem Informasi Penggajian PDAM untuk pengelolaan tenaga kerja lepas PERUMDA Tirta Sanjiwani

+
+
+
+ + + +
+
+
Manajemen Tenaga Kerja
+
Data terpusat & terstruktur
+
+
+
+
+ + + +
+
+
Penggajian Otomatis
+
Akurat & tepat waktu
+
+
+
+
+ + + +
+
+
Absensi & Kontrak
+
Monitoring real-time
+
+
+
+
+ + +
+ +
+ + + + + \ No newline at end of file diff --git a/samooapk/resources/views/auth/register.blade.php b/samooapk/resources/views/auth/register.blade.php new file mode 100644 index 0000000..1faf6a2 --- /dev/null +++ b/samooapk/resources/views/auth/register.blade.php @@ -0,0 +1,359 @@ + + + + + + Daftar - SIPDAM + + + + + + + + + + +
+ +

Bergabung dengan SIPDAM

+

Buat akun untuk mengakses sistem penggajian tenaga kerja lepas PERUMDA Tirta Sanjiwani

+
+
+
+ + + +
+
+
Manajemen Tenaga Kerja
+
Data terpusat & terstruktur
+
+
+
+
+ + + +
+
+
Penggajian Otomatis
+
Akurat & tepat waktu
+
+
+
+
+ + + +
+
+
Absensi & Kontrak
+
Monitoring real-time
+
+
+
+
+ + +
+
+ +
+ +

Buat Akun Baru

+

Isi data di bawah untuk mendaftar ke SIPDAM

+
+ +
+ @csrf + +
+ + + @error('name') +
{{ $message }}
+ @enderror +
+ +
+ + + @error('email') +
{{ $message }}
+ @enderror +
+ +
+ + + @error('password') +
{{ $message }}
+ @enderror +
+ +
+ + + @error('password_confirmation') +
{{ $message }}
+ @enderror +
+ + +
+ + + +
+
+ + + \ No newline at end of file diff --git a/samooapk/resources/views/auth/reset-password.blade.php b/samooapk/resources/views/auth/reset-password.blade.php new file mode 100644 index 0000000..44c0d00 --- /dev/null +++ b/samooapk/resources/views/auth/reset-password.blade.php @@ -0,0 +1,379 @@ + + + + + + Reset Password - SIPDAM + + + + + + + + + +
+ +

Atur Ulang Password

+

Silakan buat password baru yang kuat untuk melindungi akun Anda

+
+
+ + + +
+

Gunakan Password Kuat

+

Disarankan menggunakan kombinasi huruf, angka, dan karakter spesial agar akun Anda lebih aman.

+
+
+ + +
+
+ +
+ +
+ + + +
+

Buat Password Baru

+

Masukkan email dan password baru Anda untuk melanjutkan.

+
+ +
+ @csrf + + + + +
+ + + @error('email') +
{{ $message }}
+ @enderror +
+ +
+ +
+ + + + + + + +
+ @error('password') +
{{ $message }}
+ @enderror +
+ +
+ +
+ + + + + + + +
+ @error('password_confirmation') +
{{ $message }}
+ @enderror +
+ + +
+ +
+
+ + + + + diff --git a/samooapk/resources/views/auth/verify-email.blade.php b/samooapk/resources/views/auth/verify-email.blade.php new file mode 100644 index 0000000..eaf811d --- /dev/null +++ b/samooapk/resources/views/auth/verify-email.blade.php @@ -0,0 +1,31 @@ + +
+ {{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }} +
+ + @if (session('status') == 'verification-link-sent') +
+ {{ __('A new verification link has been sent to the email address you provided during registration.') }} +
+ @endif + +
+
+ @csrf + +
+ + {{ __('Resend Verification Email') }} + +
+
+ +
+ @csrf + + +
+
+
diff --git a/samooapk/resources/views/components/application-logo.blade.php b/samooapk/resources/views/components/application-logo.blade.php new file mode 100644 index 0000000..46579cf --- /dev/null +++ b/samooapk/resources/views/components/application-logo.blade.php @@ -0,0 +1,3 @@ + + + diff --git a/samooapk/resources/views/components/auth-session-status.blade.php b/samooapk/resources/views/components/auth-session-status.blade.php new file mode 100644 index 0000000..c4bd6e2 --- /dev/null +++ b/samooapk/resources/views/components/auth-session-status.blade.php @@ -0,0 +1,7 @@ +@props(['status']) + +@if ($status) +
merge(['class' => 'font-medium text-sm text-green-600']) }}> + {{ $status }} +
+@endif diff --git a/samooapk/resources/views/components/danger-button.blade.php b/samooapk/resources/views/components/danger-button.blade.php new file mode 100644 index 0000000..d17d288 --- /dev/null +++ b/samooapk/resources/views/components/danger-button.blade.php @@ -0,0 +1,3 @@ + diff --git a/samooapk/resources/views/components/dropdown-link.blade.php b/samooapk/resources/views/components/dropdown-link.blade.php new file mode 100644 index 0000000..e0f8ce1 --- /dev/null +++ b/samooapk/resources/views/components/dropdown-link.blade.php @@ -0,0 +1 @@ +merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out']) }}>{{ $slot }} diff --git a/samooapk/resources/views/components/dropdown.blade.php b/samooapk/resources/views/components/dropdown.blade.php new file mode 100644 index 0000000..db38742 --- /dev/null +++ b/samooapk/resources/views/components/dropdown.blade.php @@ -0,0 +1,43 @@ +@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white']) + +@php +switch ($align) { + case 'left': + $alignmentClasses = 'ltr:origin-top-left rtl:origin-top-right start-0'; + break; + case 'top': + $alignmentClasses = 'origin-top'; + break; + case 'right': + default: + $alignmentClasses = 'ltr:origin-top-right rtl:origin-top-left end-0'; + break; +} + +switch ($width) { + case '48': + $width = 'w-48'; + break; +} +@endphp + +
+
+ {{ $trigger }} +
+ + +
diff --git a/samooapk/resources/views/components/input-error.blade.php b/samooapk/resources/views/components/input-error.blade.php new file mode 100644 index 0000000..9e6da21 --- /dev/null +++ b/samooapk/resources/views/components/input-error.blade.php @@ -0,0 +1,9 @@ +@props(['messages']) + +@if ($messages) +
    merge(['class' => 'text-sm text-red-600 space-y-1']) }}> + @foreach ((array) $messages as $message) +
  • {{ $message }}
  • + @endforeach +
+@endif diff --git a/samooapk/resources/views/components/input-label.blade.php b/samooapk/resources/views/components/input-label.blade.php new file mode 100644 index 0000000..1cc65e2 --- /dev/null +++ b/samooapk/resources/views/components/input-label.blade.php @@ -0,0 +1,5 @@ +@props(['value']) + + diff --git a/samooapk/resources/views/components/modal.blade.php b/samooapk/resources/views/components/modal.blade.php new file mode 100644 index 0000000..70704c1 --- /dev/null +++ b/samooapk/resources/views/components/modal.blade.php @@ -0,0 +1,78 @@ +@props([ + 'name', + 'show' => false, + 'maxWidth' => '2xl' +]) + +@php +$maxWidth = [ + 'sm' => 'sm:max-w-sm', + 'md' => 'sm:max-w-md', + 'lg' => 'sm:max-w-lg', + 'xl' => 'sm:max-w-xl', + '2xl' => 'sm:max-w-2xl', +][$maxWidth]; +@endphp + +
+
+
+
+ +
+ {{ $slot }} +
+
diff --git a/samooapk/resources/views/components/nav-link.blade.php b/samooapk/resources/views/components/nav-link.blade.php new file mode 100644 index 0000000..5c101a2 --- /dev/null +++ b/samooapk/resources/views/components/nav-link.blade.php @@ -0,0 +1,11 @@ +@props(['active']) + +@php +$classes = ($active ?? false) + ? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out' + : 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out'; +@endphp + +merge(['class' => $classes]) }}> + {{ $slot }} + diff --git a/samooapk/resources/views/components/primary-button.blade.php b/samooapk/resources/views/components/primary-button.blade.php new file mode 100644 index 0000000..d71f0b6 --- /dev/null +++ b/samooapk/resources/views/components/primary-button.blade.php @@ -0,0 +1,3 @@ + diff --git a/samooapk/resources/views/components/responsive-nav-link.blade.php b/samooapk/resources/views/components/responsive-nav-link.blade.php new file mode 100644 index 0000000..43b91e7 --- /dev/null +++ b/samooapk/resources/views/components/responsive-nav-link.blade.php @@ -0,0 +1,11 @@ +@props(['active']) + +@php +$classes = ($active ?? false) + ? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 text-start text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition duration-150 ease-in-out' + : 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out'; +@endphp + +merge(['class' => $classes]) }}> + {{ $slot }} + diff --git a/samooapk/resources/views/components/secondary-button.blade.php b/samooapk/resources/views/components/secondary-button.blade.php new file mode 100644 index 0000000..b32b69f --- /dev/null +++ b/samooapk/resources/views/components/secondary-button.blade.php @@ -0,0 +1,3 @@ + diff --git a/samooapk/resources/views/components/text-input.blade.php b/samooapk/resources/views/components/text-input.blade.php new file mode 100644 index 0000000..1df7f0d --- /dev/null +++ b/samooapk/resources/views/components/text-input.blade.php @@ -0,0 +1,3 @@ +@props(['disabled' => false]) + +merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) !!}> diff --git a/samooapk/resources/views/dashboard.blade.php b/samooapk/resources/views/dashboard.blade.php new file mode 100644 index 0000000..2a2cdfd --- /dev/null +++ b/samooapk/resources/views/dashboard.blade.php @@ -0,0 +1,272 @@ +{{-- resources/views/dashboard.blade.php --}} +{{-- CSS sudah dipisah ke: public/css/dashboard.css --}} + + +@push('scripts') + + +@endpush + +
+ + + + {{-- ── STAT CARDS ── --}} +
+
+
+
+ + + +
+
Total Teknisi
+
{{ $totalTeknisi ?? 0 }}
+
Terdaftar dalam sistem
+
+ +
+
+
+ + + +
+
Teknisi Aktif
+
{{ $teknisiAktif ?? 0 }}
+
Sedang bertugas
+
+ +
+
+
+ + + + +
+
Total Pekerjaan
+
{{ $totalPekerjaan ?? 0 }}
+
Semua penugasan
+
+ +
+
+
+ + + +
+
Total Laporan
+
{{ $totalLaporan ?? 0 }}
+
Laporan kegiatan
+
+
+ + {{-- ── BAR CHART ── --}} +
+
+
+
Penugasan per Jenis Pekerjaan — Bulan Ini
+
+
+ @foreach($chartLabels as $index => $label) + {{ $label }} + @endforeach +
+
+ +
+
+ + {{-- ── BAR CHART TEKNISI ── --}} +
+
+
+
Beban Kerja Teknisi — Seluruh Data
+
+
+ +
+
+ + {{-- ── TARIF PEKERJAAN (FULL WIDTH) ── --}} +
+
+
+
Tarif Pekerjaan — Referensi Cepat
+
+ + + + + + + + + + + @foreach($tarifPekerjaans ?? [] as $tarif) + + + + + + + @endforeach + +
PekerjaanKodeTarifSatuan
+ @php + $badgeClass = match($tarif->jenis_pekerjaan) { + 'sr' => 'bj-sr', + 'pengembangan_jaringan_pipa', + 'penyempurnaan_jaringan_pipa' => 'bj-pjp', + 'perbaikan_jaringan_pipa' => 'bj-perbaikan', + 'gali_urug' => 'bj-gali', + 'pemasangan_gate_valve', + 'pengangkatan' => 'bj-gv', + 'pengecatan_pipa_besi' => 'bj-cat', + default => 'bj-sr', + }; + $priceColor = match($tarif->jenis_pekerjaan) { + 'sr' => '#0f6e56', + 'pengembangan_jaringan_pipa', 'penyempurnaan_jaringan_pipa'=> '#185fa5', + 'perbaikan_jaringan_pipa' => '#a32d2d', + 'gali_urug' => '#633806', + 'pengecatan_pipa_besi' => '#72243e', + default => '#3c3489', + }; + @endphp + {{ $tarif->nama_item }} + {{ $tarif->kode_item }} + Rp {{ number_format($tarif->tarif_per_unit ?? $tarif->tarif_per_meter, 0, ',', '.') }} + + {{ $tarif->tarif_per_meter ? '/meter' : '/unit' }} +
+
+ + {{-- ── BOTTOM WIDGETS ── --}} +
+ + {{-- INFO PERUMDA --}} +
+ +
Tirta Sanjiwani
+
Kabupaten Gianyar, Bali
+
+
+ + + + (0361) 943233 +
+
+ + + + tekleperumda@gmail.com +
+
+ + {{-- DONUT CHART --}} +
+
+
+
Distribusi Pekerjaan
+
+
+
+ +
+
+
+ @foreach($chartLabels as $index => $label) + {{ $label }} + @endforeach +
+
+ +
+ +
+ +
\ No newline at end of file diff --git a/samooapk/resources/views/layouts/app.blade.php b/samooapk/resources/views/layouts/app.blade.php new file mode 100644 index 0000000..4cb96ac --- /dev/null +++ b/samooapk/resources/views/layouts/app.blade.php @@ -0,0 +1,340 @@ + + + + + + + + + {{ config('app.name', 'SIPDAM') }} + + + + + + + + + + + + + + + + + @stack('styles') + + + + + + +
+ + {{-- SIDEBAR --}} + @include('layouts.navigation') + + {{-- MAIN --}} +
+ + {{-- TOPBAR --}} +
+ +
+ + + +
+
+ Selamat datang, + {{ Auth::user()->name ?? 'User' }} +
+
+ +
+ +
+ +
+ + --:--:-- +
+ +
+ +
+ + {{-- CONTENT --}} +
+ {{ $slot }} +
+ +
+ +
+ + + + + {{-- CLOCK --}} + + + {{-- SIDEBAR --}} + + + @stack('scripts') + + + + \ No newline at end of file diff --git a/samooapk/resources/views/layouts/guest.blade.php b/samooapk/resources/views/layouts/guest.blade.php new file mode 100644 index 0000000..11feb47 --- /dev/null +++ b/samooapk/resources/views/layouts/guest.blade.php @@ -0,0 +1,30 @@ + + + + + + + + {{ config('app.name', 'Laravel') }} + + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + +
+
+ + + +
+ +
+ {{ $slot }} +
+
+ + diff --git a/samooapk/resources/views/layouts/navigation.blade.php b/samooapk/resources/views/layouts/navigation.blade.php new file mode 100644 index 0000000..266be8b --- /dev/null +++ b/samooapk/resources/views/layouts/navigation.blade.php @@ -0,0 +1,149 @@ +{{-- resources/views/layouts/navigation.blade.php --}} + + \ No newline at end of file diff --git a/samooapk/resources/views/profile/edit.blade.php b/samooapk/resources/views/profile/edit.blade.php new file mode 100644 index 0000000..2a54fd1 --- /dev/null +++ b/samooapk/resources/views/profile/edit.blade.php @@ -0,0 +1,159 @@ +{{-- resources/views/profile/edit.blade.php --}} + + +@push('styles') + +@endpush + +
+
+ + {{-- ── PAGE HEADER ── --}} +
+
+ +
+
+

Profil Saya

+

Kelola detail akun dan keamanan Anda

+
+
+ + {{-- Alert Success / Status --}} + @if (session('status') === 'profile-updated') +
+ + Informasi profil berhasil diperbarui! +
+ @elseif (session('status') === 'password-updated') +
+ + Password berhasil diperbarui! +
+ @endif + +
+ + {{-- ── COLUMN 1: USER DETAILS & LOGOUT ── --}} +
+
+
+ Profile +
+ +

{{ $user->name }}

+

{{ $user->email }}

+ +
+
+ Hak Akses + Administrator +
+
+ Terdaftar + {{ $user->created_at ? \Carbon\Carbon::parse($user->created_at)->translatedFormat('d M Y') : '-' }} +
+
+ + {{-- BUTTON LOGOUT YANG BENER-BENER LOGOUT KE HALAMAN LOGIN --}} +
+ @csrf + +
+
+
+ + {{-- ── COLUMN 2: FORMS ── --}} +
+ + {{-- FORM 1: UPDATE PROFILE INFORMATION --}} +
+

+ + Informasi Profil +

+

Perbarui nama pengguna dan alamat email akun Anda.

+ +
+ @csrf + @method('patch') + +
+
+ + + @if($errors->has('name')) + {{ $errors->first('name') }} + @endif +
+ +
+ + + @if($errors->has('email')) + {{ $errors->first('email') }} + @endif +
+ + +
+
+
+ + {{-- FORM 2: UPDATE PASSWORD --}} +
+

+ + Perbarui Password +

+

Pastikan akun Anda menggunakan password yang aman dan rahasia.

+ +
+ @csrf + @method('put') + +
+
+ + + @if($errors->updatePassword->has('current_password')) + {{ $errors->updatePassword->first('current_password') }} + @endif +
+ +
+ + + @if($errors->updatePassword->has('password')) + {{ $errors->updatePassword->first('password') }} + @endif +
+ +
+ + + @if($errors->updatePassword->has('password_confirmation')) + {{ $errors->updatePassword->first('password_confirmation') }} + @endif +
+ + +
+
+
+ +
+ +
+ +
+
+ +
diff --git a/samooapk/resources/views/profile/index.blade.php b/samooapk/resources/views/profile/index.blade.php new file mode 100644 index 0000000..d08a5ea --- /dev/null +++ b/samooapk/resources/views/profile/index.blade.php @@ -0,0 +1,47 @@ +@extends('layouts.app') + +@section('title', 'Profile') +@section('page-title', 'Profile') + +@section('content') +
+
+
+
+
+ +
+
+

Admin User

+

Administrator

+ + Edit Profile + +
+
+ +
+
+
Profile Information
+
+
+ Full Name: +

Admin User

+
+
+ Email: +

admin@example.com

+
+
+ Role: +

Administrator

+
+
+ Last Login: +

2 hours ago

+
+
+
+
+
+@endsection \ No newline at end of file diff --git a/samooapk/resources/views/profile/partials/delete-user-form.blade.php b/samooapk/resources/views/profile/partials/delete-user-form.blade.php new file mode 100644 index 0000000..edeeb4a --- /dev/null +++ b/samooapk/resources/views/profile/partials/delete-user-form.blade.php @@ -0,0 +1,55 @@ +
+
+

+ {{ __('Delete Account') }} +

+ +

+ {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }} +

+
+ + {{ __('Delete Account') }} + + +
+ @csrf + @method('delete') + +

+ {{ __('Are you sure you want to delete your account?') }} +

+ +

+ {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }} +

+ +
+ + + + + +
+ +
+ + {{ __('Cancel') }} + + + + {{ __('Delete Account') }} + +
+
+
+
diff --git a/samooapk/resources/views/profile/partials/update-password-form.blade.php b/samooapk/resources/views/profile/partials/update-password-form.blade.php new file mode 100644 index 0000000..eaca1ac --- /dev/null +++ b/samooapk/resources/views/profile/partials/update-password-form.blade.php @@ -0,0 +1,48 @@ +
+
+

+ {{ __('Update Password') }} +

+ +

+ {{ __('Ensure your account is using a long, random password to stay secure.') }} +

+
+ +
+ @csrf + @method('put') + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ {{ __('Save') }} + + @if (session('status') === 'password-updated') +

{{ __('Saved.') }}

+ @endif +
+
+
diff --git a/samooapk/resources/views/profile/partials/update-profile-information-form.blade.php b/samooapk/resources/views/profile/partials/update-profile-information-form.blade.php new file mode 100644 index 0000000..5ae3d35 --- /dev/null +++ b/samooapk/resources/views/profile/partials/update-profile-information-form.blade.php @@ -0,0 +1,64 @@ +
+
+

+ {{ __('Profile Information') }} +

+ +

+ {{ __("Update your account's profile information and email address.") }} +

+
+ +
+ @csrf +
+ +
+ @csrf + @method('patch') + +
+ + + +
+ +
+ + + + + @if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->hasVerifiedEmail()) +
+

+ {{ __('Your email address is unverified.') }} + + +

+ + @if (session('status') === 'verification-link-sent') +

+ {{ __('A new verification link has been sent to your email address.') }} +

+ @endif +
+ @endif +
+ +
+ {{ __('Save') }} + + @if (session('status') === 'profile-updated') +

{{ __('Saved.') }}

+ @endif +
+
+
diff --git a/samooapk/resources/views/welcome.blade.php b/samooapk/resources/views/welcome.blade.php new file mode 100644 index 0000000..35f6395 --- /dev/null +++ b/samooapk/resources/views/welcome.blade.php @@ -0,0 +1,614 @@ + + + + + + SIPDAM - Sistem Penggajian Tenaga Kerja Lepas | PERUMDA Tirta Sanjiwani + + + + + + + + + + +
+ + + +
+ + +
+
+ + + (0361) 943233 + + + + tekleperumda@gmail.com + +
+ +
+ + + + + + +
+
+
+ + Sistem Resmi PERUMDA Tirta Sanjiwani +
+

+ Kelola Tenaga Kerja Lepas dengan Mudah & Efisien +

+

+ Platform digital terintegrasi untuk pengelolaan data, kontrak, absensi, dan penggajian tenaga kerja lepas PERUMDA Air Minum Tirta Sanjiwani Kabupaten Gianyar. +

+
+ @if (Route::has('login')) + @auth + + + Buka Dashboard + + @else + + + Masuk Sekarang + + @if (Route::has('register')) + + + Daftar Akun + + @endif + @endauth + @endif +
+
+ +
+
+ +
+ Teknisi lapangan perbaikan pipa PDAM +
+ + +
+
+ + + +
+
+
100%
+
Data Terverifikasi
+
+
+ + +
+
+ + + +
+
+
Gaji Tepat Waktu
+
Otomatis & Akurat
+
+
+
+
+
+ + +
+

Fitur Unggulan Sistem

+
+
+
+ +
+

Data Pekerja

+

Kelola data lengkap tenaga kerja lepas secara terpusat dan terstruktur

+
+
+
+ +
+

Absensi Digital

+

Pencatatan kehadiran harian yang akurat dan mudah dipantau

+
+
+
+ +
+

Manajemen Pekerjaan

+

Pemantauan penugasan dan pelaporan progres kerja teknisi secara berkala

+
+
+
+ +
+

Penggajian

+

Perhitungan dan rekap gaji otomatis berdasarkan kehadiran

+
+
+
+ + +
+ PERUMDA Air Minum Tirta Sanjiwani · Kabupaten Gianyar, Bali  |  + SIPDAM © {{ date('Y') }} · Laravel v{{ Illuminate\Foundation\Application::VERSION }} +
+ + + \ No newline at end of file diff --git a/samooapk/routes/api.php b/samooapk/routes/api.php new file mode 100644 index 0000000..66f91ff --- /dev/null +++ b/samooapk/routes/api.php @@ -0,0 +1,89 @@ +get('/user', function (Request $request) { + return $request->user(); +}); + +// Routes untuk Teknisi (Mobile App) +Route::prefix('teknisi')->group(function () { + Route::post('/login', [AkunTeknisiController::class, 'login']); + + Route::middleware('auth:api')->group(function () { + Route::post('/logout', [AkunTeknisiController::class, 'logout']); + Route::get('/me', [AkunTeknisiController::class, 'me']); + Route::post('/refresh', [AkunTeknisiController::class, 'refresh']); + Route::post('/change-password', [AkunTeknisiController::class, 'changePassword']); + }); +}); + +Route::prefix('absensi')->group(function () { + Route::post('/absen-masuk', [AbsensiApiController::class, 'absenMasuk']); + Route::post('/absen-keluar', [AbsensiApiController::class, 'absenKeluar']); + Route::get('/check-status/{id_teknisi}', [AbsensiApiController::class, 'checkStatus']); + + // ✅ Route spesifik HARUS di atas /{id} + Route::get('/riwayat', [AbsensiApiController::class, 'riwayat']); + Route::get('/statistik', [AbsensiApiController::class, 'statistik']); + Route::get('/status/options', [AbsensiApiController::class, 'getStatusOptions']); + Route::get('/rekap', [AbsensiApiController::class, 'rekap']); + Route::get('/kalender', [AbsensiApiController::class, 'kalender']); + + // ⚠️ Route dinamis /{id} paling BAWAH + // Route::get('/', [AbsensiApiController::class, 'index']); + // Route::post('/', [AbsensiApiController::class, 'store']); + // Route::get('/{id}', [AbsensiApiController::class, 'show']); + // Route::put('/{id}', [AbsensiApiController::class, 'update']); + // Route::delete('/{id}', [AbsensiApiController::class, 'destroy']); +}); + +// ✅ Routes Penugasan (BARU) +Route::prefix('penugasan')->group(function () { + + // ⚠️ Route spesifik HARUS di atas route dengan parameter dinamis! + + // Master data + Route::get('/statistik', [PenugasanApiController::class, 'statistik']); + Route::get('/master/tarif-by-jenis', [PenugasanApiController::class, 'getTarifByJenis']); + Route::get('/master/teknisi-list', [PenugasanApiController::class, 'getTeknisiList']); + + // CRUD dasar + Route::get('/', [PenugasanApiController::class, 'index']); + Route::get('/{id}', [PenugasanApiController::class, 'show']); + + // Aksi spesifik per penugasan + Route::post('/{id}/lengkapi-detail', [PenugasanApiController::class, 'lengkapiDetail']); + Route::put('/{id}/update-detail', [PenugasanApiController::class, 'updateDetail']); // ✅ BARU untuk edit + Route::post('/{id}/add-item', [PenugasanApiController::class, 'addItem']); // ✅ BARU + Route::put('/{id}/update-status', [PenugasanApiController::class, 'updateStatus']); + Route::post('/{id}/upload-foto', [PenugasanApiController::class, 'uploadFoto']); +}); + +// ✅ Routes Gaji (Mobile) +Route::prefix('gaji')->group(function () { + Route::get('/riwayat', [GajiApiController::class, 'riwayat']); + Route::get('/{id}', [GajiApiController::class, 'show']); +}); + +// ✅ Routes Kasbon (Mobile) +Route::prefix('kasbon')->group(function () { + Route::get('/riwayat', [KasbonApiController::class, 'riwayat']); + Route::get('/statistik', [KasbonApiController::class, 'statistik']); +}); + +// ✅ Routes Dashboard (Mobile) +Route::get('/dashboard', [DashboardApiController::class, 'index']); \ No newline at end of file diff --git a/samooapk/routes/auth.php b/samooapk/routes/auth.php new file mode 100644 index 0000000..1040b51 --- /dev/null +++ b/samooapk/routes/auth.php @@ -0,0 +1,59 @@ +group(function () { + Route::get('register', [RegisteredUserController::class, 'create']) + ->name('register'); + + Route::post('register', [RegisteredUserController::class, 'store']); + + Route::get('login', [AuthenticatedSessionController::class, 'create']) + ->name('login'); + + Route::post('login', [AuthenticatedSessionController::class, 'store']); + + Route::get('forgot-password', [PasswordResetLinkController::class, 'create']) + ->name('password.request'); + + Route::post('forgot-password', [PasswordResetLinkController::class, 'store']) + ->name('password.email'); + + Route::get('reset-password/{token}', [NewPasswordController::class, 'create']) + ->name('password.reset'); + + Route::post('reset-password', [NewPasswordController::class, 'store']) + ->name('password.store'); +}); + +Route::middleware('auth')->group(function () { + Route::get('verify-email', EmailVerificationPromptController::class) + ->name('verification.notice'); + + Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) + ->middleware(['signed', 'throttle:6,1']) + ->name('verification.verify'); + + Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) + ->middleware('throttle:6,1') + ->name('verification.send'); + + Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) + ->name('password.confirm'); + + Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); + + Route::put('password', [PasswordController::class, 'update'])->name('password.update'); + + Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) + ->name('logout'); +}); diff --git a/samooapk/routes/channels.php b/samooapk/routes/channels.php new file mode 100644 index 0000000..5d451e1 --- /dev/null +++ b/samooapk/routes/channels.php @@ -0,0 +1,18 @@ +id === (int) $id; +}); diff --git a/samooapk/routes/console.php b/samooapk/routes/console.php new file mode 100644 index 0000000..e05f4c9 --- /dev/null +++ b/samooapk/routes/console.php @@ -0,0 +1,19 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); diff --git a/samooapk/routes/web.php b/samooapk/routes/web.php new file mode 100644 index 0000000..40a0704 --- /dev/null +++ b/samooapk/routes/web.php @@ -0,0 +1,157 @@ +group(function () { + Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard'); + + // ===== KELOLA TEKNISI ===== + Route::prefix('teknisi')->name('teknisi.')->group(function () { + Route::get('/', [TeknisiController::class, 'index'])->name('index'); + Route::post('/', [TeknisiController::class, 'store'])->name('store'); + Route::get('/{id}', [TeknisiController::class, 'show'])->name('show'); + Route::put('/{id}', [TeknisiController::class, 'update'])->name('update'); + Route::delete('/{id}', [TeknisiController::class, 'destroy'])->name('destroy'); + + // TAMBAHKAN INI - Redirect ke route absensi yang benar + Route::get('/absensi', function() { + return redirect()->route('absensi.index'); + })->name('absensi'); + }); + + // ===== AKUN TEKNISI ===== + Route::prefix('akun-teknisi')->name('akun-teknisi.')->group(function () { + Route::get('/', [AkunTeknisiController::class, 'index'])->name('index'); + Route::post('/', [AkunTeknisiController::class, 'store'])->name('store'); + Route::get('/{id}', [AkunTeknisiController::class, 'show'])->name('show'); + Route::get('/{id}/edit', [AkunTeknisiController::class, 'edit'])->name('edit'); + Route::put('/{id}', [AkunTeknisiController::class, 'update'])->name('update'); + Route::delete('/{id}', [AkunTeknisiController::class, 'destroy'])->name('destroy'); + Route::post('/{id}/update-status', [AkunTeknisiController::class, 'updateStatus'])->name('update-status'); + }); + + // ===== ABSENSI ===== + Route::prefix('absensi')->name('absensi.')->group(function () { + Route::get('/', [AbsensiController::class, 'index'])->name('index'); + Route::get('/{id}', [AbsensiController::class, 'show'])->name('show'); + Route::put('/{id}', [AbsensiController::class, 'update'])->name('update'); + }); + + // ===== KELOLA PEKERJAAN ===== + Route::prefix('pekerjaan')->name('pekerjaan.')->group(function () { + // Data Penugasan dengan CRUD lengkap + Route::prefix('penugasan')->name('penugasan.')->group(function () { + + // ✅ TAMBAHKAN INI - Route untuk ambil tarif + Route::get('/tarif-by-kategori', [PenugasanController::class, 'getTarifByKategori']) + ->name('getTarifByKategori'); + + + Route::get('/get-teknisi', [PenugasanController::class, 'getTeknisiByDate'])->name('get-teknisi'); + Route::get('/', [PenugasanController::class, 'index'])->name('index'); + Route::post('/', [PenugasanController::class, 'store'])->name('store'); + Route::get('/{id}', [PenugasanController::class, 'show'])->name('show'); + Route::get('/{id}/edit', [PenugasanController::class, 'edit'])->name('edit'); + Route::put('/{id}', [PenugasanController::class, 'update'])->name('update'); + Route::delete('/{id}', [PenugasanController::class, 'destroy'])->name('destroy'); + Route::post('/delete-multiple', [PenugasanController::class, 'destroyMultiple'])->name('delete-multiple'); + }); + + // Submenu: Monitoring Progres + Route::get('/monitoring', [PenugasanController::class, 'monitoring'])->name('monitoring'); +}); + + // ===== GAJI ===== + Route::prefix('gaji')->group(function () { + Route::get('/perhitungan', fn() => redirect()->route('penggajian.index'))->name('gaji.perhitungan'); + Route::get('/kasbon', fn() => redirect()->route('kasbon.index'))->name('gaji.kasbon'); + }); + + Route::post('/penggajian/hitung', [PenggajianController::class, 'hitungGaji'])->name('penggajian.hitung'); + Route::post('/penggajian/bayar-semua', [PenggajianController::class, 'prosesSemuaPembayaran'])->name('penggajian.bayar-semua'); + Route::post('/penggajian/{penggajian}/bayar', [PenggajianController::class, 'prosesPembayaran'])->name('penggajian.bayar'); + Route::get('/penggajian/export', [PenggajianController::class, 'export'])->name('penggajian.export'); + Route::get('/penggajian/{penggajian}/slip', [PenggajianController::class, 'slip'])->name('penggajian.slip'); + Route::get('/penggajian/{penggajian}/detail', [PenggajianController::class, 'detail'])->name('penggajian.detail'); + Route::post('/penggajian/{penggajian}/recalculate', [PenggajianController::class, 'recalculate']) + ->name('penggajian.recalculate'); + Route::post('/penggajian/{penggajian}/update-kasbon', [PenggajianController::class, 'updateKasbon']) + ->name('penggajian.update-kasbon'); + Route::post('/penggajian/{penggajian}/update-makan', [PenggajianController::class, 'updateMakan']) + ->name('penggajian.update-makan'); + Route::resource('penggajian', PenggajianController::class); + + Route::get('/kasbon/statistics', [KasbonController::class, 'statistics'])->name('kasbon.statistics'); +Route::post('/kasbon/{id}/lunas', [KasbonController::class, 'markAsLunas'])->name('kasbon.lunas'); +Route::resource('kasbon', KasbonController::class); + + // ===== LAPORAN ===== + Route::prefix('laporan')->name('laporan.')->group(function () { + Route::get('/', [LaporanController::class, 'index'])->name('index'); + Route::get('/statistics', [LaporanController::class, 'statistics'])->name('statistics'); + Route::get('/kasbon', [LaporanController::class, 'kasbon'])->name('kasbon'); + Route::get('/teknisi', [LaporanController::class, 'teknisi'])->name('teknisi'); + Route::get('/absensi', [LaporanController::class, 'absensi'])->name('absensi'); + Route::get('/pekerjaan', [LaporanController::class, 'pekerjaan'])->name('pekerjaan'); + Route::get('/penggajian', [LaporanController::class, 'penggajian'])->name('penggajian'); + Route::get('/data-teknisi', [LaporanController::class, 'dataTeknisi'])->name('data_teknisi'); + Route::get('/export', [LaporanController::class, 'export'])->name('export'); + Route::get('/kasbon/export', [LaporanController::class, 'exportKasbon'])->name('kasbon.export'); + Route::get('/teknisi/export', [LaporanController::class, 'exportTeknisi'])->name('teknisi.export'); + Route::get('/absensi/export', [LaporanController::class, 'exportAbsensi'])->name('absensi.export'); + Route::get('/pekerjaan/export', [LaporanController::class, 'exportPekerjaan'])->name('pekerjaan.export'); + Route::get('/penggajian/export', [LaporanController::class, 'exportPenggajian'])->name('penggajian.export'); + Route::get('/data-teknisi/export', [LaporanController::class, 'exportDataTeknisi'])->name('data_teknisi.export'); + }); + + // ===== KELOLA ADMIN ===== + // Route::resource('kelola-admin', KelolaAdminController::class); + + // ===== PROFILE ===== + Route::prefix('profile')->name('profile.')->group(function () { + Route::get('/', [ProfileController::class, 'edit'])->name('edit'); + Route::patch('/', [ProfileController::class, 'update'])->name('update'); + Route::delete('/', [ProfileController::class, 'destroy'])->name('destroy'); + }); +}); + +require __DIR__.'/auth.php'; + +// Helper route to create storage link on hosting +Route::get('/create-storage-link', function () { + try { + \Illuminate\Support\Facades\Artisan::call('storage:link'); + return 'Storage link created successfully!'; + } catch (\Exception $e) { + return 'Error: ' . $e->getMessage(); + } +}); + +Route::get('/clear-cache', function () { + try { + \Illuminate\Support\Facades\Artisan::call('config:clear'); + \Illuminate\Support\Facades\Artisan::call('cache:clear'); + \Illuminate\Support\Facades\Artisan::call('view:clear'); + \Illuminate\Support\Facades\Artisan::call('route:clear'); + return 'All cache cleared successfully!'; + } catch (\Exception $e) { + return 'Error: ' . $e->getMessage(); + } +}); + diff --git a/samooapk/storage/app/.gitignore b/samooapk/storage/app/.gitignore new file mode 100644 index 0000000..8f4803c --- /dev/null +++ b/samooapk/storage/app/.gitignore @@ -0,0 +1,3 @@ +* +!public/ +!.gitignore diff --git a/samooapk/storage/app/public/.gitignore b/samooapk/storage/app/public/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/samooapk/storage/app/public/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/samooapk/storage/framework/.gitignore b/samooapk/storage/framework/.gitignore new file mode 100644 index 0000000..05c4471 --- /dev/null +++ b/samooapk/storage/framework/.gitignore @@ -0,0 +1,9 @@ +compiled.php +config.php +down +events.scanned.php +maintenance.php +routes.php +routes.scanned.php +schedule-* +services.json diff --git a/samooapk/storage/framework/cache/.gitignore b/samooapk/storage/framework/cache/.gitignore new file mode 100644 index 0000000..01e4a6c --- /dev/null +++ b/samooapk/storage/framework/cache/.gitignore @@ -0,0 +1,3 @@ +* +!data/ +!.gitignore diff --git a/samooapk/storage/framework/cache/data/.gitignore b/samooapk/storage/framework/cache/data/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/samooapk/storage/framework/cache/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/samooapk/storage/framework/sessions/.gitignore b/samooapk/storage/framework/sessions/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/samooapk/storage/framework/sessions/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/samooapk/storage/framework/testing/.gitignore b/samooapk/storage/framework/testing/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/samooapk/storage/framework/testing/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/samooapk/storage/framework/views/.gitignore b/samooapk/storage/framework/views/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/samooapk/storage/framework/views/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/samooapk/storage/logs/.gitignore b/samooapk/storage/logs/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/samooapk/storage/logs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/samooapk/tailwind.config.js b/samooapk/tailwind.config.js new file mode 100644 index 0000000..c29eb1a --- /dev/null +++ b/samooapk/tailwind.config.js @@ -0,0 +1,21 @@ +import defaultTheme from 'tailwindcss/defaultTheme'; +import forms from '@tailwindcss/forms'; + +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', + './storage/framework/views/*.php', + './resources/views/**/*.blade.php', + ], + + theme: { + extend: { + fontFamily: { + sans: ['Figtree', ...defaultTheme.fontFamily.sans], + }, + }, + }, + + plugins: [forms], +}; diff --git a/samooapk/test_hosting_gaji.php b/samooapk/test_hosting_gaji.php new file mode 100644 index 0000000..8b5a081 --- /dev/null +++ b/samooapk/test_hosting_gaji.php @@ -0,0 +1,11 @@ +make(Kernel::class)->bootstrap(); + + return $app; + } +} diff --git a/samooapk/tests/Feature/Auth/AuthenticationTest.php b/samooapk/tests/Feature/Auth/AuthenticationTest.php new file mode 100644 index 0000000..0303b29 --- /dev/null +++ b/samooapk/tests/Feature/Auth/AuthenticationTest.php @@ -0,0 +1,55 @@ +get('/login'); + + $response->assertStatus(200); + } + + public function test_users_can_authenticate_using_the_login_screen(): void + { + $user = User::factory()->create(); + + $response = $this->post('/login', [ + 'email' => $user->email, + 'password' => 'password', + ]); + + $this->assertAuthenticated(); + $response->assertRedirect(RouteServiceProvider::HOME); + } + + public function test_users_can_not_authenticate_with_invalid_password(): void + { + $user = User::factory()->create(); + + $this->post('/login', [ + 'email' => $user->email, + 'password' => 'wrong-password', + ]); + + $this->assertGuest(); + } + + public function test_users_can_logout(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/logout'); + + $this->assertGuest(); + $response->assertRedirect('/'); + } +} diff --git a/samooapk/tests/Feature/Auth/EmailVerificationTest.php b/samooapk/tests/Feature/Auth/EmailVerificationTest.php new file mode 100644 index 0000000..ba19d9c --- /dev/null +++ b/samooapk/tests/Feature/Auth/EmailVerificationTest.php @@ -0,0 +1,65 @@ +create([ + 'email_verified_at' => null, + ]); + + $response = $this->actingAs($user)->get('/verify-email'); + + $response->assertStatus(200); + } + + public function test_email_can_be_verified(): void + { + $user = User::factory()->create([ + 'email_verified_at' => null, + ]); + + Event::fake(); + + $verificationUrl = URL::temporarySignedRoute( + 'verification.verify', + now()->addMinutes(60), + ['id' => $user->id, 'hash' => sha1($user->email)] + ); + + $response = $this->actingAs($user)->get($verificationUrl); + + Event::assertDispatched(Verified::class); + $this->assertTrue($user->fresh()->hasVerifiedEmail()); + $response->assertRedirect(RouteServiceProvider::HOME.'?verified=1'); + } + + public function test_email_is_not_verified_with_invalid_hash(): void + { + $user = User::factory()->create([ + 'email_verified_at' => null, + ]); + + $verificationUrl = URL::temporarySignedRoute( + 'verification.verify', + now()->addMinutes(60), + ['id' => $user->id, 'hash' => sha1('wrong-email')] + ); + + $this->actingAs($user)->get($verificationUrl); + + $this->assertFalse($user->fresh()->hasVerifiedEmail()); + } +} diff --git a/samooapk/tests/Feature/Auth/PasswordConfirmationTest.php b/samooapk/tests/Feature/Auth/PasswordConfirmationTest.php new file mode 100644 index 0000000..ff85721 --- /dev/null +++ b/samooapk/tests/Feature/Auth/PasswordConfirmationTest.php @@ -0,0 +1,44 @@ +create(); + + $response = $this->actingAs($user)->get('/confirm-password'); + + $response->assertStatus(200); + } + + public function test_password_can_be_confirmed(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/confirm-password', [ + 'password' => 'password', + ]); + + $response->assertRedirect(); + $response->assertSessionHasNoErrors(); + } + + public function test_password_is_not_confirmed_with_invalid_password(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/confirm-password', [ + 'password' => 'wrong-password', + ]); + + $response->assertSessionHasErrors(); + } +} diff --git a/samooapk/tests/Feature/Auth/PasswordResetTest.php b/samooapk/tests/Feature/Auth/PasswordResetTest.php new file mode 100644 index 0000000..aa50350 --- /dev/null +++ b/samooapk/tests/Feature/Auth/PasswordResetTest.php @@ -0,0 +1,73 @@ +get('/forgot-password'); + + $response->assertStatus(200); + } + + public function test_reset_password_link_can_be_requested(): void + { + Notification::fake(); + + $user = User::factory()->create(); + + $this->post('/forgot-password', ['email' => $user->email]); + + Notification::assertSentTo($user, ResetPassword::class); + } + + public function test_reset_password_screen_can_be_rendered(): void + { + Notification::fake(); + + $user = User::factory()->create(); + + $this->post('/forgot-password', ['email' => $user->email]); + + Notification::assertSentTo($user, ResetPassword::class, function ($notification) { + $response = $this->get('/reset-password/'.$notification->token); + + $response->assertStatus(200); + + return true; + }); + } + + public function test_password_can_be_reset_with_valid_token(): void + { + Notification::fake(); + + $user = User::factory()->create(); + + $this->post('/forgot-password', ['email' => $user->email]); + + Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { + $response = $this->post('/reset-password', [ + 'token' => $notification->token, + 'email' => $user->email, + 'password' => 'password', + 'password_confirmation' => 'password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect(route('login')); + + return true; + }); + } +} diff --git a/samooapk/tests/Feature/Auth/PasswordUpdateTest.php b/samooapk/tests/Feature/Auth/PasswordUpdateTest.php new file mode 100644 index 0000000..ca28c6c --- /dev/null +++ b/samooapk/tests/Feature/Auth/PasswordUpdateTest.php @@ -0,0 +1,51 @@ +create(); + + $response = $this + ->actingAs($user) + ->from('/profile') + ->put('/password', [ + 'current_password' => 'password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/profile'); + + $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); + } + + public function test_correct_password_must_be_provided_to_update_password(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->from('/profile') + ->put('/password', [ + 'current_password' => 'wrong-password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]); + + $response + ->assertSessionHasErrorsIn('updatePassword', 'current_password') + ->assertRedirect('/profile'); + } +} diff --git a/samooapk/tests/Feature/Auth/RegistrationTest.php b/samooapk/tests/Feature/Auth/RegistrationTest.php new file mode 100644 index 0000000..f6d5818 --- /dev/null +++ b/samooapk/tests/Feature/Auth/RegistrationTest.php @@ -0,0 +1,33 @@ +get('/register'); + + $response->assertRedirect('/login'); + $response->assertSessionHas('error', 'regis sudah ditutup hubungi mandor aja.'); + } + + public function test_new_users_cannot_register(): void + { + $response = $this->post('/register', [ + 'name' => 'Test User', + 'email' => 'test@example.com', + 'password' => 'password', + 'password_confirmation' => 'password', + ]); + + $this->assertGuest(); + $response->assertRedirect('/login'); + $response->assertSessionHas('error', 'regis sudah ditutup hubungi mandor aja.'); + } +} diff --git a/samooapk/tests/Feature/ExampleTest.php b/samooapk/tests/Feature/ExampleTest.php new file mode 100644 index 0000000..8364a84 --- /dev/null +++ b/samooapk/tests/Feature/ExampleTest.php @@ -0,0 +1,19 @@ +get('/'); + + $response->assertStatus(200); + } +} diff --git a/samooapk/tests/Feature/ProfileTest.php b/samooapk/tests/Feature/ProfileTest.php new file mode 100644 index 0000000..252fdcc --- /dev/null +++ b/samooapk/tests/Feature/ProfileTest.php @@ -0,0 +1,99 @@ +create(); + + $response = $this + ->actingAs($user) + ->get('/profile'); + + $response->assertOk(); + } + + public function test_profile_information_can_be_updated(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->patch('/profile', [ + 'name' => 'Test User', + 'email' => 'test@example.com', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/profile'); + + $user->refresh(); + + $this->assertSame('Test User', $user->name); + $this->assertSame('test@example.com', $user->email); + $this->assertNull($user->email_verified_at); + } + + public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->patch('/profile', [ + 'name' => 'Test User', + 'email' => $user->email, + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/profile'); + + $this->assertNotNull($user->refresh()->email_verified_at); + } + + public function test_user_can_delete_their_account(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->delete('/profile', [ + 'password' => 'password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/'); + + $this->assertGuest(); + $this->assertNull($user->fresh()); + } + + public function test_correct_password_must_be_provided_to_delete_account(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->from('/profile') + ->delete('/profile', [ + 'password' => 'wrong-password', + ]); + + $response + ->assertSessionHasErrorsIn('userDeletion', 'password') + ->assertRedirect('/profile'); + + $this->assertNotNull($user->fresh()); + } +} diff --git a/samooapk/tests/TestCase.php b/samooapk/tests/TestCase.php new file mode 100644 index 0000000..2932d4a --- /dev/null +++ b/samooapk/tests/TestCase.php @@ -0,0 +1,10 @@ +assertTrue(true); + } +} diff --git a/samooapk/vite.config.js b/samooapk/vite.config.js new file mode 100644 index 0000000..89f26f5 --- /dev/null +++ b/samooapk/vite.config.js @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite'; +import laravel from 'laravel-vite-plugin'; + +export default defineConfig({ + plugins: [ + laravel({ + input: [ + 'resources/css/app.css', + 'resources/js/app.js', + ], + refresh: true, + }), + ], +}); diff --git a/samooflutter/.gitignore b/samooflutter/.gitignore new file mode 100644 index 0000000..79c113f --- /dev/null +++ b/samooflutter/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/samooflutter/.metadata b/samooflutter/.metadata new file mode 100644 index 0000000..6a623a4 --- /dev/null +++ b/samooflutter/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "d7b523b356d15fb81e7d340bbe52b47f93937323" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: android + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: ios + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: linux + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: macos + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: web + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: windows + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/samooflutter/README.md b/samooflutter/README.md new file mode 100644 index 0000000..2c8a8ae --- /dev/null +++ b/samooflutter/README.md @@ -0,0 +1,16 @@ +# flutter_application_1 + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/samooflutter/analysis_options.yaml b/samooflutter/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/samooflutter/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/samooflutter/android/.gitignore b/samooflutter/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/samooflutter/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/samooflutter/android/app/build.gradle.kts b/samooflutter/android/app/build.gradle.kts new file mode 100644 index 0000000..1cd211d --- /dev/null +++ b/samooflutter/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.flutter_application_1" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.samoo.pdamteknisi" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/samooflutter/android/app/src/debug/AndroidManifest.xml b/samooflutter/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/samooflutter/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/samooflutter/android/app/src/main/AndroidManifest.xml b/samooflutter/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..bb5c4ea --- /dev/null +++ b/samooflutter/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samooflutter/android/app/src/main/kotlin/com/example/flutter_application_1/MainActivity.kt b/samooflutter/android/app/src/main/kotlin/com/example/flutter_application_1/MainActivity.kt new file mode 100644 index 0000000..aa48e52 --- /dev/null +++ b/samooflutter/android/app/src/main/kotlin/com/example/flutter_application_1/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.flutter_application_1 + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/samooflutter/android/app/src/main/res/drawable-v21/launch_background.xml b/samooflutter/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/samooflutter/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/samooflutter/android/app/src/main/res/drawable/launch_background.xml b/samooflutter/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/samooflutter/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/samooflutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/samooflutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/samooflutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/samooflutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/samooflutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/samooflutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/samooflutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/samooflutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/samooflutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/samooflutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/samooflutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/samooflutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/samooflutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/samooflutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/samooflutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/samooflutter/android/app/src/main/res/values-night/styles.xml b/samooflutter/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/samooflutter/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/samooflutter/android/app/src/main/res/values/styles.xml b/samooflutter/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/samooflutter/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/samooflutter/android/app/src/profile/AndroidManifest.xml b/samooflutter/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/samooflutter/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/samooflutter/android/build.gradle.kts b/samooflutter/android/build.gradle.kts new file mode 100644 index 0000000..89176ef --- /dev/null +++ b/samooflutter/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/samooflutter/android/gradle.properties b/samooflutter/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/samooflutter/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/samooflutter/android/gradle/wrapper/gradle-wrapper.properties b/samooflutter/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ac3b479 --- /dev/null +++ b/samooflutter/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/samooflutter/android/settings.gradle.kts b/samooflutter/android/settings.gradle.kts new file mode 100644 index 0000000..ab39a10 --- /dev/null +++ b/samooflutter/android/settings.gradle.kts @@ -0,0 +1,25 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/samooflutter/ios/.gitignore b/samooflutter/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/samooflutter/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/samooflutter/ios/Flutter/AppFrameworkInfo.plist b/samooflutter/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/samooflutter/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/samooflutter/ios/Flutter/Debug.xcconfig b/samooflutter/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/samooflutter/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/samooflutter/ios/Flutter/Release.xcconfig b/samooflutter/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/samooflutter/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/samooflutter/ios/Runner.xcodeproj/project.pbxproj b/samooflutter/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..6669684 --- /dev/null +++ b/samooflutter/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterApplication1; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterApplication1.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterApplication1.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterApplication1.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterApplication1; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterApplication1; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/samooflutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/samooflutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/samooflutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/samooflutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/samooflutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/samooflutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/samooflutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/samooflutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/samooflutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/samooflutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/samooflutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/samooflutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samooflutter/ios/Runner.xcworkspace/contents.xcworkspacedata b/samooflutter/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/samooflutter/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/samooflutter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/samooflutter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/samooflutter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/samooflutter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/samooflutter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/samooflutter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/samooflutter/ios/Runner/AppDelegate.swift b/samooflutter/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/samooflutter/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/samooflutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/samooflutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/samooflutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/samooflutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/samooflutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/samooflutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/samooflutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/samooflutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/samooflutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/samooflutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/samooflutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/samooflutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/samooflutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/samooflutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/samooflutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/samooflutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/samooflutter/ios/Runner/Base.lproj/LaunchScreen.storyboard b/samooflutter/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/samooflutter/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samooflutter/ios/Runner/Base.lproj/Main.storyboard b/samooflutter/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/samooflutter/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samooflutter/ios/Runner/Info.plist b/samooflutter/ios/Runner/Info.plist new file mode 100644 index 0000000..34cea35 --- /dev/null +++ b/samooflutter/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + PDAM Teknisi + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + PDAM Teknisi + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/samooflutter/ios/Runner/Runner-Bridging-Header.h b/samooflutter/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/samooflutter/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/samooflutter/ios/RunnerTests/RunnerTests.swift b/samooflutter/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/samooflutter/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/samooflutter/lib/absensi/absensi.dart b/samooflutter/lib/absensi/absensi.dart new file mode 100644 index 0000000..8f13d05 --- /dev/null +++ b/samooflutter/lib/absensi/absensi.dart @@ -0,0 +1,1100 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:image_picker/image_picker.dart'; +import '../api/AbsensiApi.dart'; +import 'package:geolocator/geolocator.dart'; +import '../api/LoginApi.dart'; +import 'package:intl/intl.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:timezone/timezone.dart' as tz; +import 'package:timezone/data/latest.dart' as tz; + +// ── Palette ─────────────────────────────────────────────────────────────────── +const _bg = Color(0xFFF9FAFB); +const _bg1 = Color(0xFFFFFFFF); +const _bg2 = Color(0xFFF3F4F6); +const _green = Color(0xFF10B981); +const _greenDim = Color(0x1A10B981); +const _greenGlow = Color(0x4D10B981); +const _cyan = Color(0xFF06B6D4); +const _cyanDim = Color(0x1A06B6D4); +const _amber = Color(0xFFF59E0B); +const _amberDim = Color(0x1AF59E0B); +const _rose = Color(0xFFEF4444); +const _roseDim = Color(0x1AEF4444); +const _t1 = Color(0xFF111827); +const _t2 = Color(0xFF6B7280); +const _t3 = Color(0xFF9CA3AF); +const _line2 = Color(0xFFE5E7EB); + +class AbsensiScreen extends StatefulWidget { + const AbsensiScreen({Key? key}) : super(key: key); + + @override + State createState() => _AbsensiScreenState(); +} + +class _AbsensiScreenState extends State + with TickerProviderStateMixin { + final AbsensiApi _absensiApi = AbsensiApi(); + final ApiService _apiService = ApiService(); + final ImagePicker _picker = ImagePicker(); + + bool _isLoading = true; + bool _isLoadingRiwayat = false; + bool _isLoadingRekap = false; + bool _isProcessing = false; + + Map? _userData; + Map? _statusAbsensi; + int? _idTeknisi; + + List> _riwayat = []; + Map _rekap = {}; + Map _kalenderData = {}; + + String _filterStatus = 'semua'; + DateTime _bulanRiwayat = DateTime(DateTime.now().year, DateTime.now().month); + DateTime _bulanRekap = DateTime(DateTime.now().year, DateTime.now().month); + DateTime _bulanKalender = DateTime(DateTime.now().year, DateTime.now().month); + + late TabController _tabCtrl; + late AnimationController _pulseCtrl; + late Animation _pulseAnim; + + final List> _statusOptions = [ + {'label': 'Hadir', 'value': 'hadir', 'icon': Icons.check_circle_rounded, 'color': _green}, + {'label': 'Izin', 'value': 'izin', 'icon': Icons.event_busy_rounded, 'color': _amber}, + {'label': 'Sakit', 'value': 'sakit', 'icon': Icons.local_hospital_rounded, 'color': _rose}, + ]; + + @override + void initState() { + super.initState(); + tz.initializeTimeZones(); + _tabCtrl = TabController(length: 2, vsync: this); + _pulseCtrl = AnimationController( + vsync: this, duration: const Duration(seconds: 2)) + ..repeat(reverse: true); + _pulseAnim = Tween(begin: 0.4, end: 1.0).animate( + CurvedAnimation(parent: _pulseCtrl, curve: Curves.easeInOut)); + + _tabCtrl.addListener(() { + if (!_tabCtrl.indexIsChanging) { + if (_tabCtrl.index == 1) { + if (_rekap.isEmpty) _fetchRekap(); + if (_riwayat.isEmpty) _fetchRiwayat(); + } + } + }); + + _loadData(); + } + + @override + void dispose() { + _tabCtrl.dispose(); + _pulseCtrl.dispose(); + super.dispose(); + } + + // ── API calls ───────────────────────────────────────────────────────────── + + Future _loadData() async { + setState(() => _isLoading = true); + final res = await _apiService.getProfile(); + if (res['success'] == true && res['data'] != null) { + setState(() { + _userData = res['data']; + _idTeknisi = res['data']['teknisi']?['id_teknisi']; + }); + if (_idTeknisi != null) await _checkStatusAbsensi(); + } + setState(() => _isLoading = false); + } + + Future _checkStatusAbsensi() async { + if (_idTeknisi == null) return; + final r = await _absensiApi.checkStatus(_idTeknisi!); + if (r['success'] == true) setState(() => _statusAbsensi = r['data']); + } + + /// Ambil riwayat absensi dari API berdasarkan bulan yang dipilih + Future _fetchRiwayat() async { + if (_idTeknisi == null) return; + setState(() => _isLoadingRiwayat = true); + try { + final r = await _absensiApi.getRiwayat( + idTeknisi: _idTeknisi!, + bulan: _bulanRekap.month, // Menggunakan bulan dari rekap agar sinkron + tahun: _bulanRekap.year, + ); + if (r['success'] == true) { + setState(() { + _riwayat = List>.from(r['data'] ?? []); + }); + } + } catch (_) { + } finally { + setState(() { + _isLoadingRiwayat = false; + // Update kalender data dari riwayat + _kalenderData.clear(); + for (var item in _riwayat) { + final tgl = item['tanggal'] as String?; + if (tgl != null) { + final date = DateTime.parse(tgl); + _kalenderData[date.day] = item['status'] as String; + } + } + }); + } + } + + /// Ambil rekap bulanan dari API + Future _fetchRekap() async { + if (_idTeknisi == null) return; + setState(() => _isLoadingRekap = true); + try { + final r = await _absensiApi.getRekap( + idTeknisi: _idTeknisi!, + bulan: _bulanRekap.month, + tahun: _bulanRekap.year, + ); + if (r['success'] == true) { + setState(() => _rekap = Map.from(r['data'] ?? {})); + } + } catch (_) { + } finally { + setState(() => _isLoadingRekap = false); + } + } + + // ── Absen handlers ──────────────────────────────────────────────────────── + + Future _getCurrentPosition() async { + bool serviceEnabled; + LocationPermission permission; + + serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + _toast('Layanan lokasi dinonaktifkan.', ok: false); + return null; + } + + permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied) { + _toast('Izin lokasi ditolak', ok: false); + return null; + } + } + + if (permission == LocationPermission.deniedForever) { + _toast('Izin lokasi ditolak secara permanen.', ok: false); + return null; + } + + _toast('Mencari lokasi GPS...', ok: true); + return await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.high); + } + + Future _handleAbsenMasuk() async { + if (_idTeknisi == null) { _toast('ID Teknisi tidak ditemukan', ok: false); return; } + final result = await _showStatusDialog('Absen Masuk'); + if (result == null) return; + final status = result['status'] as String; + final ket = result['keterangan'] as String?; + XFile? image; + if (status == 'hadir') { + image = await _pickImage(); + if (image == null) { _toast('Foto diperlukan untuk status Hadir', ok: false); return; } + } + + Position? position = await _getCurrentPosition(); + if (position == null) return; + + setState(() => _isProcessing = true); + final r = await _absensiApi.absenMasuk( + idTeknisi: _idTeknisi!, fotoAbsenMasuk: image, + status: status, keterangan: ket, + latitude: position.latitude, longitude: position.longitude); + setState(() => _isProcessing = false); + if (r['success'] == true) { + _toast('Absen masuk berhasil!', ok: true); + await _checkStatusAbsensi(); + _fetchRekap(); // Refresh rekap + _fetchRiwayat(); // Refresh riwayat + } else { + _toast(r['message'] ?? 'Gagal absen masuk', ok: false); + } + } + + Future _handleAbsenKeluar() async { + if (_idTeknisi == null) { _toast('ID Teknisi tidak ditemukan', ok: false); return; } + + // Langsung minta foto tanpa dialog status untuk absen keluar + final image = await _pickImage(); + if (image == null) { + _toast('Foto diperlukan untuk Absen Keluar', ok: false); + return; + } + + Position? position = await _getCurrentPosition(); + if (position == null) return; + + setState(() => _isProcessing = true); + final r = await _absensiApi.absenKeluar( + idTeknisi: _idTeknisi!, + fotoAbsenKeluar: image, + status: 'hadir', // Otomatis hadir jika absen keluar + keterangan: null, + latitude: position.latitude, longitude: position.longitude); + + setState(() => _isProcessing = false); + if (r['success'] == true) { + _toast('Absen keluar berhasil!', ok: true); + await _checkStatusAbsensi(); + _fetchRekap(); // Refresh rekap + _fetchRiwayat(); // Refresh riwayat + } else { + _toast(r['message'] ?? 'Gagal absen keluar', ok: false); + } + } + + Future _pickImage() async { + if (kIsWeb) return _picker.pickImage(source: ImageSource.gallery, imageQuality: 70); + final src = await _showImageSourceDialog(); + if (src == null) return null; + return _picker.pickImage(source: src, imageQuality: 70); + } + + // ── Dialogs ─────────────────────────────────────────────────────────────── + + Future _showImageSourceDialog() { + return showDialog( + context: context, + builder: (ctx) => Dialog( + backgroundColor: _bg1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: const BorderSide(color: _line2)), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Container(width: 48, height: 48, + decoration: BoxDecoration(color: _cyanDim, + borderRadius: BorderRadius.circular(13), + border: Border.all(color: _cyan.withOpacity(0.3))), + child: const Icon(Icons.camera_alt_rounded, color: _cyan, size: 22)), + const SizedBox(height: 12), + const Text('Pilih Sumber Foto', style: TextStyle( + fontSize: 16, fontWeight: FontWeight.w700, color: _t1)), + const SizedBox(height: 18), + _srcBtn(Icons.camera_alt_rounded, 'Kamera', _cyan, _cyanDim, + () => Navigator.pop(ctx, ImageSource.camera)), + const SizedBox(height: 8), + _srcBtn(Icons.photo_library_rounded, 'Galeri', _green, _greenDim, + () => Navigator.pop(ctx, ImageSource.gallery)), + const SizedBox(height: 4), + TextButton(onPressed: () => Navigator.pop(ctx), + child: const Text('Batal', style: TextStyle(color: _t2))), + ]), + ), + ), + ); + } + + Widget _srcBtn(IconData icon, String label, Color c, Color dim, VoidCallback fn) { + return Material(color: dim, borderRadius: BorderRadius.circular(11), + child: InkWell(onTap: fn, borderRadius: BorderRadius.circular(11), + child: Container(width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 16), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(11), + border: Border.all(color: c.withOpacity(0.25))), + child: Row(children: [ + Icon(icon, color: c, size: 18), const SizedBox(width: 10), + Text(label, style: TextStyle(color: c, + fontWeight: FontWeight.w600, fontSize: 14)), + ]), + ), + ), + ); + } + + Future?> _showStatusDialog(String title) { + String? selected; + final ketCtrl = TextEditingController(); + final isIn = title.contains('Masuk'); + + return showDialog>( + context: context, + builder: (ctx) => StatefulBuilder(builder: (ctx, setSt) { + return Dialog( + backgroundColor: _bg1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: const BorderSide(color: _line2)), + child: SingleChildScrollView( + padding: const EdgeInsets.all(22), + child: Column(mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(children: [ + Container(width: 42, height: 42, + decoration: BoxDecoration( + color: isIn ? _greenDim : _roseDim, + borderRadius: BorderRadius.circular(11), + border: Border.all( + color: (isIn ? _green : _rose).withOpacity(0.3))), + child: Icon( + isIn ? Icons.login_rounded : Icons.logout_rounded, + color: isIn ? _green : _rose, size: 20)), + const SizedBox(width: 12), + Text(title, style: const TextStyle( + fontSize: 16, fontWeight: FontWeight.w700, color: _t1)), + const Spacer(), + GestureDetector(onTap: () => Navigator.pop(ctx), + child: const Icon(Icons.close_rounded, color: _t3, size: 20)), + ]), + const SizedBox(height: 18), + const Text('PILIH STATUS', style: TextStyle( + fontSize: 10, fontWeight: FontWeight.w700, + color: _t3, letterSpacing: 1.3)), + const SizedBox(height: 10), + ..._statusOptions.map((opt) { + final isSel = selected == opt['value']; + final Color c = opt['color'] as Color; + return GestureDetector( + onTap: () => setSt(() { + selected = opt['value'] as String; + if (selected != 'izin') ketCtrl.clear(); + }), + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.symmetric( + horizontal: 14, vertical: 11), + decoration: BoxDecoration( + color: isSel ? c.withOpacity(0.08) : _bg2, + borderRadius: BorderRadius.circular(11), + border: Border.all( + color: isSel ? c.withOpacity(0.45) : _line2, + width: isSel ? 1.5 : 1)), + child: Row(children: [ + Icon(opt['icon'] as IconData, + color: isSel ? c : _t3, size: 19), + const SizedBox(width: 11), + Text(opt['label'] as String, style: TextStyle( + color: isSel ? c : _t2, fontSize: 14, + fontWeight: isSel + ? FontWeight.w700 : FontWeight.w400)), + const Spacer(), + if (isSel) + Container(width: 17, height: 17, + decoration: BoxDecoration( + color: c, shape: BoxShape.circle), + child: const Icon(Icons.check, + color: Colors.black, size: 11)), + ]), + ), + ); + }).toList(), + if (selected == 'izin') ...[ + const SizedBox(height: 12), + const Text('KETERANGAN IZIN', style: TextStyle( + fontSize: 10, fontWeight: FontWeight.w700, + color: _t3, letterSpacing: 1.3)), + const SizedBox(height: 8), + TextField( + controller: ketCtrl, maxLines: 3, + style: const TextStyle(color: _t1, fontSize: 13), + cursorColor: _amber, + decoration: InputDecoration( + hintText: 'Tulis alasan izin...', + hintStyle: const TextStyle(color: _t3, fontSize: 13), + filled: true, fillColor: _bg2, + contentPadding: const EdgeInsets.all(12), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(11), + borderSide: BorderSide.none), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(11), + borderSide: const BorderSide(color: _line2)), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(11), + borderSide: + const BorderSide(color: _amber, width: 1.5)), + ), + ), + ], + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 11, vertical: 9), + decoration: BoxDecoration( + color: _bg2, borderRadius: BorderRadius.circular(9), + border: Border.all(color: _line2)), + child: Row(children: [ + Icon(Icons.info_outline_rounded, size: 14, + color: selected == 'hadir' ? _green : _t3), + const SizedBox(width: 7), + Expanded(child: Text( + selected == 'hadir' + ? 'Foto akan diminta setelah ini' + : 'Foto tidak diperlukan untuk status ini', + style: TextStyle(fontSize: 12, + color: selected == 'hadir' ? _green : _t3), + )), + ]), + ), + const SizedBox(height: 18), + Row(children: [ + Expanded(child: OutlinedButton( + onPressed: () => Navigator.pop(ctx), + style: OutlinedButton.styleFrom( + foregroundColor: _t2, + side: const BorderSide(color: _line2), + padding: const EdgeInsets.symmetric(vertical: 13), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(11))), + child: const Text('Batal'), + )), + const SizedBox(width: 10), + Expanded(flex: 2, child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + decoration: BoxDecoration( + color: selected != null + ? (isIn ? _green : _rose) : _bg2, + borderRadius: BorderRadius.circular(11), + boxShadow: selected != null ? [BoxShadow( + color: (isIn ? _green : _rose).withOpacity(0.28), + blurRadius: 12, + offset: const Offset(0, 4))] : []), + child: Material(color: Colors.transparent, child: InkWell( + borderRadius: BorderRadius.circular(11), + onTap: selected == null ? null : () { + if (selected == 'izin' && + ketCtrl.text.trim().isEmpty) { + _toast('Keterangan izin harus diisi', ok: false); + return; + } + Navigator.pop(ctx, { + 'status': selected, + 'keterangan': selected == 'izin' + ? ketCtrl.text.trim() : null, + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 13), + child: Text('Lanjutkan', + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 14, + color: selected != null + ? Colors.black : _t3)), + ), + )), + )), + ]), + ]), + ), + ); + }), + ); + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + void _toast(String msg, {required bool ok}) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Row(children: [ + Icon(ok ? Icons.check_circle_outline : Icons.error_outline, + color: ok ? _green : _rose, size: 16), + const SizedBox(width: 8), + Expanded(child: Text(msg, + style: const TextStyle(color: _t1, fontSize: 13))), + ]), + backgroundColor: _bg1, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide( + color: ok ? _green.withOpacity(0.4) : _rose.withOpacity(0.4))), + duration: const Duration(seconds: 3), + )); + } + + String _formatTime(String? t) { + if (t == null) return '--:--'; + try { + final jakarta = tz.getLocation('Asia/Jakarta'); + return DateFormat('HH:mm') + .format(tz.TZDateTime.from(DateTime.parse(t), jakarta)); + } catch (_) { return t; } + } + + Color _statusColor(String s) { + switch (s) { + case 'hadir': return _green; + case 'izin': return _amber; + case 'sakit': return _rose; + default: return _t3; + } + } + + Color _statusBg(String s) { + switch (s) { + case 'hadir': return _greenDim; + case 'izin': return _amberDim; + case 'sakit': return _roseDim; + default: return const Color(0x08ffffff); + } + } + + Widget _pill(String text, Color bg, Color fg) => Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(9), + border: Border.all(color: fg.withOpacity(0.25))), + child: Text(text, style: TextStyle( + fontSize: 10, fontWeight: FontWeight.w600, color: fg)), + ); + + Widget _navBtn(IconData icon, VoidCallback onTap) => GestureDetector( + onTap: onTap, + child: Container( + width: 34, height: 34, + decoration: BoxDecoration(color: _bg2, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: _line2)), + child: Icon(icon, color: _t2, size: 18), + ), + ); + + String _fmtBulan(DateTime d) => + DateFormat('MMMM yyyy', 'id_ID').format(d); + + // ═══════════════════════════════════════════════════════════════════════════ + // BUILD + // ═══════════════════════════════════════════════════════════════════════════ + + @override + Widget build(BuildContext context) { + final nama = (_userData?['teknisi']?['nama_teknisi'] ?? 'Teknisi') as String; + final id = _userData?['teknisi']?['id_teknisi'] ?? '-'; + final sudahIn = _statusAbsensi?['sudah_absen_masuk'] ?? false; + final sudahOut = _statusAbsensi?['sudah_absen_keluar'] ?? false; + final data = _statusAbsensi?['data_absensi']; + final initial = nama.isNotEmpty ? nama[0].toUpperCase() : 'T'; + + return Scaffold( + backgroundColor: _bg, + appBar: _buildAppBar(), + body: _isLoading + ? const Center(child: Column(mainAxisSize: MainAxisSize.min, children: [ + SizedBox(width: 36, height: 36, + child: CircularProgressIndicator( + color: _green, strokeWidth: 2.5)), + SizedBox(height: 14), + Text('Memuat data…', + style: TextStyle(color: _t2, fontSize: 13)), + ])) + : Column(children: [ + // Tab bar + Container( + color: _bg1, + child: TabBar( + controller: _tabCtrl, + indicatorColor: _green, + indicatorWeight: 3, + labelColor: _green, + unselectedLabelColor: _t3, + labelStyle: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w800, + letterSpacing: 0.5), + unselectedLabelStyle: const TextStyle( + fontSize: 13, fontWeight: FontWeight.w600), + tabs: const [ + Tab(text: 'ABSEN'), + Tab(text: 'REKAP'), + ], + ), + ), + const Divider(height: 1, color: _line2), + Expanded( + child: TabBarView( + controller: _tabCtrl, + children: [ + _buildTabAbsen( + initial, nama, id, sudahIn, sudahOut, data), + _buildTabRekap(), + ], + ), + ), + ]), + ); + } + + PreferredSizeWidget _buildAppBar() { + return AppBar( + elevation: 0, + backgroundColor: _bg1, + surfaceTintColor: Colors.transparent, + systemOverlayStyle: const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.light), + title: const Text('Absensi Teknisi', style: TextStyle( + fontSize: 16, fontWeight: FontWeight.w800, color: _t1, letterSpacing: 0.2)), + actions: [ + IconButton( + icon: const Icon(Icons.refresh_rounded, color: _t2, size: 22), + onPressed: _loadData, + ), + const SizedBox(width: 8), + ], + ); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // TAB 1 — ABSEN + // ═══════════════════════════════════════════════════════════════════════════ + + Widget _buildTabAbsen(String initial, String nama, dynamic id, + bool sudahIn, bool sudahOut, Map? data) { + final now = DateTime.now(); + final timeStr = DateFormat('HH:mm').format(now); + final dateStr = DateFormat('EEEE, d MMMM yyyy', 'id_ID').format(now); + final jadwal = _userData?['teknisi']?['jadwal_masuk'] ?? '07:30'; + final jamMasuk = data != null ? _formatTime(data['jam_masuk']) : '--:--'; + final jamKeluar = data != null ? _formatTime(data['jam_keluar']) : '--:--'; + final durasi = data?['durasi_kerja_formatted'] ?? '--'; + + return RefreshIndicator( + onRefresh: _loadData, color: _green, backgroundColor: _bg1, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), + child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + // Greeting & Profile + Row(children: [ + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Selamat ${now.hour < 11 ? 'Pagi' : now.hour < 15 ? 'Siang' : now.hour < 18 ? 'Sore' : 'Malam'},', + style: const TextStyle(color: _t2, fontSize: 13, fontWeight: FontWeight.w500)), + const SizedBox(height: 2), + Text(nama, style: const TextStyle(color: _t1, fontSize: 18, fontWeight: FontWeight.w800)), + ]), + const Spacer(), + _pill('ID: $id', _greenDim, _green), + ]), + + const SizedBox(height: 30), + + // Central Clock Display + Center( + child: Column(children: [ + Text(timeStr, style: const TextStyle(fontSize: 64, fontWeight: FontWeight.w900, color: _t1, letterSpacing: -2, height: 1)), + Text(dateStr, style: const TextStyle(color: _t2, fontSize: 14, fontWeight: FontWeight.w500)), + const SizedBox(height: 12), + ]), + ), + + const SizedBox(height: 40), + + // Log Aktivitas Card + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: _bg1, + borderRadius: BorderRadius.circular(24), + border: Border.all(color: _line2), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 20, offset: const Offset(0, 10))], + ), + child: Column(children: [ + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + const Text('LOG AKTIVITAS', style: TextStyle(color: _t3, fontSize: 11, fontWeight: FontWeight.w800, letterSpacing: 1.5)), + if (data != null) + _pill( + sudahOut ? 'Selesai' : (data['status'] == 'hadir' ? 'Sedang Bekerja' : data['status'].toUpperCase()), + sudahOut ? _greenDim : (data['status'] == 'hadir' ? _cyanDim : _roseDim), + sudahOut ? _green : (data['status'] == 'hadir' ? _cyan : _rose) + ), + ]), + const SizedBox(height: 20), + + Row(children: [ + _logItem('JADWAL', jadwal, Icons.alarm_rounded, _cyan), + _vLine(), + _logItem('MASUK', jamMasuk, Icons.login_rounded, sudahIn ? _green : _t3), + ]), + const Padding(padding: EdgeInsets.symmetric(vertical: 15), child: Divider(color: _line2, height: 1)), + Row(children: [ + _logItem('KELUAR', jamKeluar, Icons.logout_rounded, sudahOut ? _rose : _t3), + _vLine(), + _logItem('DURASI', durasi, Icons.timer_outlined, _amber), + ]), + ]), + ), + + const SizedBox(height: 32), + + // Floating Action Area + _buildActionArea(sudahIn, sudahOut, data), + ]), + ), + ); + } + + Widget _logItem(String label, String value, IconData icon, Color color) { + return Expanded( + child: Row(children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(10)), + child: Icon(icon, color: color, size: 18), + ), + const SizedBox(width: 12), + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(label, style: const TextStyle(color: _t3, fontSize: 9, fontWeight: FontWeight.w700)), + const SizedBox(height: 2), + Text(value, style: TextStyle(color: color == _t3 ? _t3 : _t1, fontSize: 16, fontWeight: FontWeight.w800)), + ]), + ]), + ); + } + + Widget _vLine() => Container(width: 1, height: 30, color: _line2, margin: const EdgeInsets.symmetric(horizontal: 10)); + + Widget _buildActionArea(bool sudahIn, bool sudahOut, Map? data) { + if (sudahIn && sudahOut) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), + decoration: BoxDecoration(color: _greenDim, borderRadius: BorderRadius.circular(16), border: Border.all(color: _green.withOpacity(0.2))), + child: const Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(Icons.check_circle_rounded, color: _green, size: 20), + const SizedBox(width: 10), + Text('Kerja hari ini selesai', style: TextStyle(color: _green, fontWeight: FontWeight.w700, fontSize: 14)), + ]), + ); + } + + if (data != null && (data['status'] == 'izin' || data['status'] == 'sakit')) { + final Color c = data['status'] == 'sakit' ? _rose : _amber; + return Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), + decoration: BoxDecoration(color: c.withOpacity(0.1), borderRadius: BorderRadius.circular(16), border: Border.all(color: c.withOpacity(0.2))), + child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(data['status'] == 'sakit' ? Icons.local_hospital_rounded : Icons.event_busy_rounded, color: c, size: 20), + const SizedBox(width: 10), + Text('Status: ${data['status'].toUpperCase()}', style: TextStyle(color: c, fontWeight: FontWeight.w700, fontSize: 14)), + ]), + ); + } + + final String label = !sudahIn ? 'Absen Masuk' : 'Absen Keluar'; + final Color color = !sudahIn ? _green : _rose; + final IconData ico = !sudahIn ? Icons.fingerprint_rounded : Icons.logout_rounded; + final VoidCallback fn = !sudahIn ? _handleAbsenMasuk : _handleAbsenKeluar; + + return Container( + height: 64, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: LinearGradient(colors: [color, color.withBlue(150)], begin: Alignment.topLeft, end: Alignment.bottomRight), + boxShadow: [BoxShadow(color: color.withOpacity(0.3), blurRadius: 20, offset: const Offset(0, 8))], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(20), + onTap: _isProcessing ? null : fn, + child: Center( + child: _isProcessing + ? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(color: Colors.black, strokeWidth: 3)) + : Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(ico, color: Colors.black, size: 24), + const SizedBox(width: 12), + Text(label.toUpperCase(), style: const TextStyle(color: Colors.black, fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 1)), + ]), + ), + ), + ), + ); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // TAB 2 — REKAP & RIWAYAT + // ═══════════════════════════════════════════════════════════════════════════ + + Widget _buildTabRekap() { + return _isLoadingRekap + ? const Center(child: CircularProgressIndicator(color: _green, strokeWidth: 2.5)) + : _rekap.isEmpty + ? Center(child: Column(mainAxisSize: MainAxisSize.min, children: [ + Container(width: 56, height: 56, + decoration: BoxDecoration(color: _bg2, borderRadius: BorderRadius.circular(14), border: Border.all(color: _line2)), + child: const Icon(Icons.bar_chart_rounded, color: _t3, size: 26)), + const SizedBox(height: 14), + const Text('Tidak ada data rekap', style: TextStyle(color: _t2, fontSize: 14)), + const SizedBox(height: 8), + GestureDetector( + onTap: () { _fetchRekap(); _fetchRiwayat(); }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration(color: _greenDim, borderRadius: BorderRadius.circular(8), border: Border.all(color: _green.withOpacity(0.3))), + child: const Text('Muat Rekap', style: TextStyle(color: _green, fontSize: 12, fontWeight: FontWeight.w600)), + ), + ), + ])) + : _buildRekapContent(); + } + + Widget _buildRekapContent() { + final pct = (_rekap['persentase'] as num?)?.toDouble() ?? 0.0; + final hadir = (_rekap['hadir'] as num?)?.toInt() ?? 0; + final izin = (_rekap['izin'] as num?)?.toInt() ?? 0; + final sakit = (_rekap['sakit'] as num?)?.toInt() ?? 0; + final alpha = (_rekap['alpha'] as num?)?.toInt() ?? 0; + final total = (_rekap['total_hari_kerja'] as num?)?.toInt() ?? 1; + + return CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 24, 20, 16), + child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + // Bulan nav + Row(children: [ + Text(_rekap['bulan']?.toString() ?? _fmtBulan(_bulanRekap), + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w800, color: _t1)), + const Spacer(), + _navBtn(Icons.chevron_left_rounded, () { + setState(() { + _bulanRekap = DateTime(_bulanRekap.year, _bulanRekap.month - 1); + _rekap = {}; _riwayat = []; + }); + _fetchRekap(); _fetchRiwayat(); + }), + const SizedBox(width: 8), + _navBtn(Icons.chevron_right_rounded, () { + setState(() { + _bulanRekap = DateTime(_bulanRekap.year, _bulanRekap.month + 1); + _rekap = {}; _riwayat = []; + }); + _fetchRekap(); _fetchRiwayat(); + }), + ]), + const SizedBox(height: 20), + + // Calendar Grid + _buildCalendarGrid(), + + const SizedBox(height: 24), + + const SizedBox(height: 16), + + // 3 Stats Grid + Row(children: [ + Expanded(child: _rekapStatCard('$hadir', 'Hadir', _green, hadir/total)), + const SizedBox(width: 10), + Expanded(child: _rekapStatCard('$izin', 'Izin', _amber, izin/total)), + const SizedBox(width: 10), + Expanded(child: _rekapStatCard('$sakit', 'Sakit', _rose, sakit/total)), + ]), + + const SizedBox(height: 32), + const Text('RIWAYAT HARIAN', style: TextStyle(fontSize: 11, color: _t3, fontWeight: FontWeight.w800, letterSpacing: 1.5)), + const SizedBox(height: 16), + ]), + ), + ), + + // Riwayat List + if (_isLoadingRiwayat) + const SliverFillRemaining(child: Center(child: CircularProgressIndicator(color: _green))) + else if (_riwayat.isEmpty) + const SliverToBoxAdapter( + child: Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 40), + child: Text('Tidak ada riwayat bulan ini', style: TextStyle(color: _t3)), + ), + ), + ) + else + SliverPadding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 40), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => _buildHistoryItem(_riwayat[i]), + childCount: _riwayat.length, + ), + ), + ), + ], + ); + } + + Widget _rekapStatCard(String val, String label, Color color, double frac) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 4), + decoration: BoxDecoration(color: _bg1, borderRadius: BorderRadius.circular(16), border: Border.all(color: color.withOpacity(0.15))), + child: Column(children: [ + Text(val, style: TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: color)), + const SizedBox(height: 2), + Text(label, style: const TextStyle(fontSize: 9, color: _t2, fontWeight: FontWeight.w700)), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: ClipRRect( + borderRadius: BorderRadius.circular(100), + child: LinearProgressIndicator(value: frac.clamp(0.0, 1.0), minHeight: 2, backgroundColor: color.withOpacity(0.1), valueColor: AlwaysStoppedAnimation(color)), + ), + ), + ]), + ); + } + + Widget _buildHistoryItem(Map item) { + final status = (item['status'] ?? 'alpha') as String; + final color = _statusColor(status); + final bg = _statusBg(status); + final masuk = item['jam_masuk_formatted'] as String?; + final keluar = item['jam_keluar_formatted'] as String?; + final tanggal = item['tanggal'] as String? ?? ''; + final dayNum = tanggal.length >= 10 ? tanggal.substring(8, 10) : '--'; + final dayName = tanggal.isNotEmpty ? DateFormat('EEEE', 'id_ID').format(DateTime.parse(tanggal)) : '-'; + + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration(color: _bg1, borderRadius: BorderRadius.circular(16), border: Border.all(color: _line2)), + child: Row(children: [ + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(dayNum, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: _t1)), + Text(dayName.substring(0, 3).toUpperCase(), style: const TextStyle(fontSize: 10, color: _t3, fontWeight: FontWeight.w800)), + ]), + const SizedBox(width: 20), + Container(width: 1, height: 30, color: _line2), + const SizedBox(width: 20), + Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (status == 'libur') + const Text('Hari Libur', style: TextStyle(fontSize: 14, color: _t3, fontStyle: FontStyle.italic)) + else + Row(children: [ + Text(masuk ?? '--:--', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w800, color: masuk != null ? _t1 : _t3)), + const Padding(padding: EdgeInsets.symmetric(horizontal: 8), child: Text('→', style: TextStyle(color: _t3))), + Text(keluar ?? '--:--', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w800, color: keluar != null ? _t1 : _t3)), + ]), + const SizedBox(height: 2), + Text(status == 'hadir' ? (keluar != null ? 'Selesai' : 'Sedang Bekerja') : status.toUpperCase(), + style: TextStyle(fontSize: 10, color: color, fontWeight: FontWeight.w700)), + ])), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(100), border: Border.all(color: color.withOpacity(0.2))), + child: Text(status.toUpperCase(), style: TextStyle(fontSize: 9, fontWeight: FontWeight.w900, color: color)), + ), + ]), + ); + } + Widget _buildCalendarGrid() { + final daysInMonth = DateTime(_bulanRekap.year, _bulanRekap.month + 1, 0).day; + final firstDay = DateTime(_bulanRekap.year, _bulanRekap.month, 1).weekday; + + // Day labels + const weekDays = ['Sn', 'Sl', 'Rb', 'Km', 'Jm', 'Sb', 'Mg']; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _bg1, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: _line2), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: weekDays.map((d) => Text(d, style: const TextStyle(color: _t3, fontSize: 11, fontWeight: FontWeight.w800))).toList(), + ), + const SizedBox(height: 12), + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 7, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + ), + itemCount: daysInMonth + (firstDay - 1), + itemBuilder: (context, index) { + if (index < firstDay - 1) return const SizedBox(); + + final day = index - (firstDay - 2); + final status = _kalenderData[day]; + + Color dotColor = Colors.transparent; + Color textColor = _t2; + BoxDecoration? deco; + + if (status != null) { + dotColor = _statusColor(status); + textColor = _t1; + deco = BoxDecoration( + color: _statusBg(status), + shape: BoxShape.circle, + border: Border.all(color: dotColor.withOpacity(0.3)), + ); + } + + return Center( + child: Container( + width: 32, + height: 32, + decoration: deco, + child: Center( + child: Text('$day', + style: TextStyle( + color: textColor, + fontSize: 12, + fontWeight: status != null ? FontWeight.w900 : FontWeight.w400 + ) + ), + ), + ), + ); + }, + ), + const SizedBox(height: 16), + // Legend + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _legendItem('Masuk', _green), + const SizedBox(width: 12), + _legendItem('Izin', _amber), + const SizedBox(width: 12), + _legendItem('Sakit', _rose), + const SizedBox(width: 12), + _legendItem('Alfa', _t3), + ], + ) + ], + ), + ); + } + + Widget _legendItem(String label, Color color) { + return Row( + children: [ + Container(width: 6, height: 6, decoration: BoxDecoration(color: color, shape: BoxShape.circle)), + const SizedBox(width: 4), + Text(label, style: const TextStyle(color: _t3, fontSize: 9, fontWeight: FontWeight.w700)), + ], + ); + } +} \ No newline at end of file diff --git a/samooflutter/lib/api/AbsensiApi.dart b/samooflutter/lib/api/AbsensiApi.dart new file mode 100644 index 0000000..08ea5d2 --- /dev/null +++ b/samooflutter/lib/api/AbsensiApi.dart @@ -0,0 +1,363 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; + +class AbsensiApi { + static const String baseUrl = 'https://ta.myhost.id/E31230906/api'; + + static const String absenMasukEndpoint = '/absensi/absen-masuk'; + static const String absenKeluarEndpoint = '/absensi/absen-keluar'; + static const String checkStatusEndpoint = '/absensi/check-status'; + static const String riwayatEndpoint = '/absensi/riwayat'; + static const String kalenderEndpoint = '/absensi/kalender'; + static const String rekapEndpoint = '/absensi/rekap'; + static const Duration timeoutDuration = Duration(seconds: 30); + + Future _getToken() async { + try { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('access_token'); + } catch (e) { + print('Error getting token: $e'); + return null; + } + } + + Future> _getMultipartHeaders() async { + final token = await _getToken(); + return { + 'Accept': 'application/json', + if (token != null) 'Authorization': 'Bearer $token', + }; + } + + Future> _getHeaders() async { + final token = await _getToken(); + return { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + if (token != null) 'Authorization': 'Bearer $token', + }; + } + + /// Absen masuk dengan support Web & Mobile + Future> absenMasuk({ + required int idTeknisi, + XFile? fotoAbsenMasuk, + String? status, + String? keterangan, + double? latitude, + double? longitude, + }) async { + try { + print('=== ABSEN MASUK REQUEST ==='); + print('ID Teknisi: $idTeknisi'); + print('Status: ${status ?? "null"}'); + print('Keterangan: ${keterangan ?? "null"}'); + print('Foto: ${fotoAbsenMasuk != null ? "Ada (${fotoAbsenMasuk.name})" : "TIDAK ADA"}'); + + var request = http.MultipartRequest( + 'POST', + Uri.parse('$baseUrl$absenMasukEndpoint'), + ); + + request.headers.addAll(await _getMultipartHeaders()); + request.fields['id_teknisi'] = idTeknisi.toString(); + + if (status != null && status.isNotEmpty) { + request.fields['status'] = status; + } + + if (keterangan != null && keterangan.isNotEmpty) { + request.fields['keterangan'] = keterangan; + } + + if (latitude != null) { + request.fields['latitude'] = latitude.toString(); + } + + if (longitude != null) { + request.fields['longitude'] = longitude.toString(); + } + + if (fotoAbsenMasuk != null) { + if (kIsWeb) { + final bytes = await fotoAbsenMasuk.readAsBytes(); + print('📸 Foto size (Web): ${bytes.length} bytes'); + request.files.add(http.MultipartFile.fromBytes( + 'foto_absen_masuk', bytes, + filename: fotoAbsenMasuk.name, + )); + } else { + print('📸 Foto path (Mobile): ${fotoAbsenMasuk.path}'); + request.files.add(await http.MultipartFile.fromPath( + 'foto_absen_masuk', fotoAbsenMasuk.path, + )); + } + } + + print('📤 Sending request to: $baseUrl$absenMasukEndpoint'); + final streamedResponse = await request.send().timeout(timeoutDuration); + final response = await http.Response.fromStream(streamedResponse); + + print('=== ABSEN MASUK RESPONSE ==='); + print('Status Code: ${response.statusCode}'); + print('Response Body: ${response.body}'); + + final data = jsonDecode(response.body); + + if (response.statusCode == 201 && data['success'] == true) { + print('✅ ABSEN MASUK SUKSES!'); + return {'success': true, 'message': data['message'], 'data': data['data']}; + } else if (response.statusCode == 400) { + print('❌ Error 400: ${data['message']}'); + return {'success': false, + 'message': data['message'] ?? 'Anda sudah melakukan absen masuk hari ini'}; + } else if (response.statusCode == 422) { + print('❌ Validation Error: ${data['errors']}'); + String errorMessage = 'Validasi gagal'; + if (data['errors'] != null) { + final errors = data['errors'] as Map; + errorMessage = errors.values.first[0]; + } + return {'success': false, 'message': errorMessage, 'errors': data['errors']}; + } else { + print('❌ Unknown Error: ${response.statusCode}'); + return {'success': false, + 'message': data['message'] ?? 'Gagal melakukan absen masuk'}; + } + } catch (e, stackTrace) { + print('❌ EXCEPTION di absenMasuk: $e'); + print(stackTrace); + return {'success': false, 'message': 'Terjadi kesalahan: ${e.toString()}'}; + } + } + + /// Absen keluar dengan support Web & Mobile + Future> absenKeluar({ + required int idTeknisi, + XFile? fotoAbsenKeluar, + String? status, + String? keterangan, + double? latitude, + double? longitude, + }) async { + try { + print('=== ABSEN KELUAR REQUEST ==='); + print('ID Teknisi: $idTeknisi'); + print('Status: ${status ?? "null"}'); + print('Keterangan: ${keterangan ?? "null"}'); + print('Foto: ${fotoAbsenKeluar != null ? "Ada (${fotoAbsenKeluar.name})" : "TIDAK ADA"}'); + + var request = http.MultipartRequest( + 'POST', + Uri.parse('$baseUrl$absenKeluarEndpoint'), + ); + + request.headers.addAll(await _getMultipartHeaders()); + request.fields['id_teknisi'] = idTeknisi.toString(); + + if (status != null && status.isNotEmpty) { + request.fields['status'] = status; + } + + if (keterangan != null && keterangan.isNotEmpty) { + request.fields['keterangan'] = keterangan; + } + + if (latitude != null) { + request.fields['latitude'] = latitude.toString(); + } + + if (longitude != null) { + request.fields['longitude'] = longitude.toString(); + } + + if (fotoAbsenKeluar != null) { + if (kIsWeb) { + final bytes = await fotoAbsenKeluar.readAsBytes(); + print('📸 Foto size (Web): ${bytes.length} bytes'); + request.files.add(http.MultipartFile.fromBytes( + 'foto_absen_keluar', bytes, + filename: fotoAbsenKeluar.name, + )); + } else { + print('📸 Foto path (Mobile): ${fotoAbsenKeluar.path}'); + request.files.add(await http.MultipartFile.fromPath( + 'foto_absen_keluar', fotoAbsenKeluar.path, + )); + } + } + + print('📤 Sending request to: $baseUrl$absenKeluarEndpoint'); + final streamedResponse = await request.send().timeout(timeoutDuration); + final response = await http.Response.fromStream(streamedResponse); + + print('=== ABSEN KELUAR RESPONSE ==='); + print('Status Code: ${response.statusCode}'); + print('Response Body: ${response.body}'); + + final data = jsonDecode(response.body); + + if (response.statusCode == 200 && data['success'] == true) { + print('✅ ABSEN KELUAR SUKSES!'); + return {'success': true, 'message': data['message'], 'data': data['data']}; + } else if (response.statusCode == 400) { + print('❌ Error 400: ${data['message']}'); + return {'success': false, + 'message': data['message'] ?? 'Belum melakukan absen masuk atau sudah absen keluar'}; + } else if (response.statusCode == 422) { + print('❌ Validation Error: ${data['errors']}'); + String errorMessage = 'Validasi gagal'; + if (data['errors'] != null) { + final errors = data['errors'] as Map; + errorMessage = errors.values.first[0]; + } + return {'success': false, 'message': errorMessage, 'errors': data['errors']}; + } else { + print('❌ Unknown Error: ${response.statusCode}'); + return {'success': false, + 'message': data['message'] ?? 'Gagal melakukan absen keluar'}; + } + } catch (e, stackTrace) { + print('❌ EXCEPTION di absenKeluar: $e'); + print(stackTrace); + return {'success': false, 'message': 'Terjadi kesalahan: ${e.toString()}'}; + } + } + + /// Cek status absensi hari ini + Future> checkStatus(int idTeknisi) async { + try { + final token = await _getToken(); + final response = await http.get( + Uri.parse('$baseUrl$checkStatusEndpoint/$idTeknisi'), + headers: { + 'Accept': 'application/json', + if (token != null) 'Authorization': 'Bearer $token', + }, + ).timeout(timeoutDuration); + + final data = jsonDecode(response.body); + + if (response.statusCode == 200 && data['success'] == true) { + return {'success': true, 'message': data['message'], 'data': data['data']}; + } else if (response.statusCode == 404) { + return {'success': false, 'message': 'Teknisi tidak ditemukan'}; + } else { + return {'success': false, + 'message': data['message'] ?? 'Gagal mengecek status absensi'}; + } + } catch (e) { + return {'success': false, 'message': 'Terjadi kesalahan: ${e.toString()}'}; + } + } + + // ── Method baru ──────────────────────────────────────────────────────────── + + /// Ambil riwayat absensi per bulan + /// Endpoint: GET /api/absensi/riwayat?id_teknisi=1&bulan=3&tahun=2026 + Future> getRiwayat({ + required int idTeknisi, + required int bulan, + required int tahun, + }) async { + try { + final url = Uri.parse( + '$baseUrl$riwayatEndpoint?id_teknisi=$idTeknisi&bulan=$bulan&tahun=$tahun'); + print('📋 GET Riwayat: $url'); + + final response = await http.get(url, + headers: await _getHeaders()).timeout(timeoutDuration); + + print('Riwayat status: ${response.statusCode}'); + print('Riwayat body: ${response.body}'); + + final data = jsonDecode(response.body); + + if (response.statusCode == 200 && data['success'] == true) { + return {'success': true, 'data': data['data'] ?? []}; + } + return { + 'success': false, + 'message': data['message'] ?? 'Gagal memuat riwayat', + 'data': [], + }; + } catch (e) { + print('❌ getRiwayat error: $e'); + return {'success': false, 'message': e.toString(), 'data': []}; + } + } + + /// Ambil status absensi per tanggal dalam 1 bulan (untuk kalender) + /// Endpoint: GET /api/absensi/kalender?id_teknisi=1&bulan=3&tahun=2026 + /// Response data: { "1": "hadir", "5": "izin", "10": "alpha", ... } + Future> getKalender({ + required int idTeknisi, + required int bulan, + required int tahun, + }) async { + try { + final url = Uri.parse( + '$baseUrl$kalenderEndpoint?id_teknisi=$idTeknisi&bulan=$bulan&tahun=$tahun'); + print('📅 GET Kalender: $url'); + + final response = await http.get(url, + headers: await _getHeaders()).timeout(timeoutDuration); + + print('Kalender status: ${response.statusCode}'); + print('Kalender body: ${response.body}'); + + final data = jsonDecode(response.body); + + if (response.statusCode == 200 && data['success'] == true) { + return {'success': true, 'data': data['data'] ?? {}}; + } + return { + 'success': false, + 'message': data['message'] ?? 'Gagal memuat kalender', + 'data': {}, + }; + } catch (e) { + print('❌ getKalender error: $e'); + return {'success': false, 'message': e.toString(), 'data': {}}; + } + } + + /// Ambil rekap absensi bulanan + /// Endpoint: GET /api/absensi/rekap?id_teknisi=1&bulan=3&tahun=2026 + /// Response data: { "bulan": "Maret 2026", "hadir": 14, "izin": 1, ... } + Future> getRekap({ + required int idTeknisi, + required int bulan, + required int tahun, + }) async { + try { + final url = Uri.parse( + '$baseUrl$rekapEndpoint?id_teknisi=$idTeknisi&bulan=$bulan&tahun=$tahun'); + print('📊 GET Rekap: $url'); + + final response = await http.get(url, + headers: await _getHeaders()).timeout(timeoutDuration); + + print('Rekap status: ${response.statusCode}'); + print('Rekap body: ${response.body}'); + + final data = jsonDecode(response.body); + + if (response.statusCode == 200 && data['success'] == true) { + return {'success': true, 'data': data['data'] ?? {}}; + } + return { + 'success': false, + 'message': data['message'] ?? 'Gagal memuat rekap', + 'data': {}, + }; + } catch (e) { + print('❌ getRekap error: $e'); + return {'success': false, 'message': e.toString(), 'data': {}}; + } + } +} \ No newline at end of file diff --git a/samooflutter/lib/api/GajiApi.dart b/samooflutter/lib/api/GajiApi.dart new file mode 100644 index 0000000..535aa63 --- /dev/null +++ b/samooflutter/lib/api/GajiApi.dart @@ -0,0 +1,61 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'LoginApi.dart'; // To reuse baseUrl if needed, or define locally + +class GajiApi { + // Gunakan baseUrl yang sama dengan LoginApi + static const String baseUrl = 'https://ta.myhost.id/E31230906/api'; + + static const Duration timeoutDuration = Duration(seconds: 30); + + Future _getToken() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('access_token'); + } + + Future _getIdTeknisi() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getInt('id_teknisi'); + } + + Future> getRiwayat() async { + try { + final token = await _getToken(); + final idTeknisi = await _getIdTeknisi(); + + final response = await http.get( + Uri.parse('$baseUrl/gaji/riwayat?id_teknisi=$idTeknisi'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer $token', + }, + ).timeout(timeoutDuration); + + return jsonDecode(response.body); + } catch (e) { + return {'success': false, 'message': e.toString()}; + } + } + + Future> getDetail(int id) async { + try { + final token = await _getToken(); + final idTeknisi = await _getIdTeknisi(); + + final response = await http.get( + Uri.parse('$baseUrl/gaji/$id?id_teknisi=$idTeknisi'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer $token', + }, + ).timeout(timeoutDuration); + + return jsonDecode(response.body); + } catch (e) { + return {'success': false, 'message': e.toString()}; + } + } +} diff --git a/samooflutter/lib/api/KasbonApi.dart b/samooflutter/lib/api/KasbonApi.dart new file mode 100644 index 0000000..ac197d1 --- /dev/null +++ b/samooflutter/lib/api/KasbonApi.dart @@ -0,0 +1,60 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; + +class KasbonApi { + static const String baseUrl = 'https://ta.myhost.id/E31230906/api'; + + static const Duration timeoutDuration = Duration(seconds: 30); + + Future _getToken() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('access_token'); + } + + Future _getIdTeknisi() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getInt('id_teknisi'); + } + + Future> getRiwayat() async { + try { + final token = await _getToken(); + final idTeknisi = await _getIdTeknisi(); + + final response = await http.get( + Uri.parse('$baseUrl/kasbon/riwayat?id_teknisi=$idTeknisi'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer $token', + }, + ).timeout(timeoutDuration); + + return jsonDecode(response.body); + } catch (e) { + return {'success': false, 'message': e.toString()}; + } + } + + Future> getStatistik() async { + try { + final token = await _getToken(); + final idTeknisi = await _getIdTeknisi(); + + // Dashboard API also contains this info + final response = await http.get( + Uri.parse('$baseUrl/kasbon/statistik?id_teknisi=$idTeknisi'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer $token', + }, + ).timeout(timeoutDuration); + + return jsonDecode(response.body); + } catch (e) { + return {'success': false, 'message': e.toString()}; + } + } +} diff --git a/samooflutter/lib/api/LoginApi.dart b/samooflutter/lib/api/LoginApi.dart new file mode 100644 index 0000000..13733ac --- /dev/null +++ b/samooflutter/lib/api/LoginApi.dart @@ -0,0 +1,324 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; + +class ApiService { + // Ganti localhost ke IP PC Anda jika running di emulator/HP asli + static const String baseUrl = 'https://ta.myhost.id/E31230906/api'; + + + static const String loginEndpoint = '/teknisi/login'; + static const String logoutEndpoint = '/teknisi/logout'; + static const String meEndpoint = '/teknisi/me'; + static const String refreshEndpoint = '/teknisi/refresh'; + static const String changePasswordEndpoint = '/teknisi/change-password'; + static const String dashboardEndpoint = '/dashboard'; + static const String gajiRiwayatEndpoint = '/gaji/riwayat'; + static const String kasbonRiwayatEndpoint = '/kasbon/riwayat'; + + static const Duration timeoutDuration = Duration(seconds: 30); + + // =================================== + // TOKEN + // =================================== + + Future getToken() async { + try { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('access_token'); + } catch (e) { + print('Error getting token: $e'); + return null; + } + } + + Future saveToken(String token) async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('access_token', token); + await prefs.setString('auth_token', token); + } catch (e) { + print('Error saving token: $e'); + } + } + + Future removeToken() async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('access_token'); + await prefs.remove('auth_token'); + } catch (e) { + print('Error removing token: $e'); + } + } + + // =================================== + // USER DATA + // =================================== + + Future saveUserData(Map userData) async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('user_data', jsonEncode(userData)); + + final teknisi = userData['teknisi']; + if (teknisi != null && teknisi['id_teknisi'] != null) { + await prefs.setInt('id_teknisi', teknisi['id_teknisi']); + } + } catch (e) { + print('Error saving user data: $e'); + } + } + + Future?> getUserData() async { + try { + final prefs = await SharedPreferences.getInstance(); + final userDataString = prefs.getString('user_data'); + if (userDataString != null) { + return jsonDecode(userDataString); + } + return null; + } catch (e) { + print('Error getting user data: $e'); + return null; + } + } + + Future removeUserData() async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('user_data'); + await prefs.remove('auth_token'); + await prefs.remove('id_teknisi'); + } catch (e) { + print('Error removing user data: $e'); + } + } + + // =================================== + // AUTH CHECK + // =================================== + + Future isLoggedIn() async { + final token = await getToken(); + return token != null && token.isNotEmpty; + } + + // =================================== + // LOGIN + // =================================== + + Future> login(String username, String password) async { + try { + final response = await http.post( + Uri.parse('$baseUrl$loginEndpoint'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: jsonEncode({ + 'username': username, + 'password': password, + }), + ).timeout(timeoutDuration); + + final data = jsonDecode(response.body); + + if (response.statusCode == 200 && data['success'] == true) { + if (data['access_token'] != null) { + await saveToken(data['access_token']); + } + if (data['user'] != null) { + await saveUserData(data['user']); + } + return { + 'success': true, + 'message': data['message'] ?? 'Login berhasil', + 'data': data, + }; + } else { + return { + 'success': false, + 'message': data['message'] ?? 'Login gagal', + }; + } + } catch (e) { + return { + 'success': false, + 'message': 'Terjadi kesalahan: ${e.toString()}', + }; + } + } + + // =================================== + // LOGOUT + // =================================== + + Future> logout() async { + try { + final token = await getToken(); + if (token != null) { + await http.post( + Uri.parse('$baseUrl$logoutEndpoint'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer $token', + }, + ).timeout(timeoutDuration); + } + await removeToken(); + await removeUserData(); + return {'success': true, 'message': 'Logout berhasil'}; + } catch (e) { + await removeToken(); + await removeUserData(); + return {'success': true, 'message': 'Logout berhasil'}; + } + } + + // =================================== + // GET PROFILE + // =================================== + + Future> getProfile() async { + try { + final token = await getToken(); + if (token == null) return {'success': false, 'message': 'Token tidak ditemukan', 'logout': true}; + final response = await http.get( + Uri.parse('$baseUrl$meEndpoint'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer $token', + }, + ).timeout(timeoutDuration); + final data = jsonDecode(response.body); + if (response.statusCode == 200 && data['success'] == true) { + if (data['data'] != null) await saveUserData(data['data']); + return {'success': true, 'message': data['message'] ?? 'Data berhasil diambil', 'data': data['data']}; + } else if (response.statusCode == 401) { + await removeToken(); + await removeUserData(); + return {'success': false, 'message': 'Sesi expired', 'logout': true}; + } + return {'success': false, 'message': 'Gagal mengambil data'}; + } catch (e) { + return {'success': false, 'message': e.toString()}; + } + } + + // =================================== + // CHANGE PASSWORD + // =================================== + + Future> changePassword({ + required String oldPassword, + required String newPassword, + required String confirmPassword, + }) async { + try { + final token = await getToken(); + if (token == null) return {'success': false, 'message': 'Auth error'}; + final response = await http.post( + Uri.parse('$baseUrl$changePasswordEndpoint'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer $token', + }, + body: jsonEncode({ + 'password_lama': oldPassword, + 'password_baru': newPassword, + 'password_baru_confirmation': confirmPassword, + }), + ).timeout(timeoutDuration); + final data = jsonDecode(response.body); + return {'success': data['success'] ?? false, 'message': data['message'] ?? 'Gagal'}; + } catch (e) { + return {'success': false, 'message': e.toString()}; + } + } + + // =================================== + // DASHBOARD & FINANCE + // =================================== + + Future> getDashboardData() async { + try { + final token = await getToken(); + final prefs = await SharedPreferences.getInstance(); + final idTeknisi = prefs.getInt('id_teknisi'); + if (token == null || idTeknisi == null) return {'success': false, 'message': 'Auth error'}; + final response = await http.get( + Uri.parse('$baseUrl$dashboardEndpoint?id_teknisi=$idTeknisi'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer $token', + }, + ).timeout(timeoutDuration); + return jsonDecode(response.body); + } catch (e) { return {'success': false, 'message': e.toString()}; } + } + + Future> getGajiRiwayat() async { + try { + final token = await getToken(); + final prefs = await SharedPreferences.getInstance(); + final idTeknisi = prefs.getInt('id_teknisi'); + + // Pastikan endpoint benar: /gaji/riwayat?id_teknisi=... + final url = Uri.parse('$baseUrl$gajiRiwayatEndpoint?id_teknisi=$idTeknisi'); + print('Fetching Gaji from: $url'); // Debug info + + final response = await http.get( + url, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer $token', + }, + ).timeout(timeoutDuration); + + return jsonDecode(response.body); + } catch (e) { + print('Error Gaji API: $e'); + return {'success': false, 'message': e.toString()}; + } + } + + Future> getGajiDetail(int id) async { + try { + final token = await getToken(); + final prefs = await SharedPreferences.getInstance(); + final idTeknisi = prefs.getInt('id_teknisi'); + final response = await http.get( + Uri.parse('$baseUrl/gaji/$id?id_teknisi=$idTeknisi'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer $token', + }, + ).timeout(timeoutDuration); + return jsonDecode(response.body); + } catch (e) { return {'success': false, 'message': e.toString()}; } + } + + Future> getKasbonRiwayat() async { + try { + final token = await getToken(); + final prefs = await SharedPreferences.getInstance(); + final idTeknisi = prefs.getInt('id_teknisi'); + final response = await http.get( + Uri.parse('$baseUrl$kasbonRiwayatEndpoint?id_teknisi=$idTeknisi'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer $token', + }, + ).timeout(timeoutDuration); + return jsonDecode(response.body); + } catch (e) { return {'success': false, 'message': e.toString()}; } + } +} \ No newline at end of file diff --git a/samooflutter/lib/api/PenugasanApi.dart b/samooflutter/lib/api/PenugasanApi.dart new file mode 100644 index 0000000..59120ba --- /dev/null +++ b/samooflutter/lib/api/PenugasanApi.dart @@ -0,0 +1,393 @@ +import 'dart:io'; +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:image_picker/image_picker.dart'; + +class PenugasanApi { +static const String baseUrl = 'https://ta.myhost.id/E31230906/api'; + + static final PenugasanApi _instance = PenugasanApi._internal(); + factory PenugasanApi() => _instance; + PenugasanApi._internal(); + + // ───────────────────────────────────────── + // PRIVATE HELPERS + // ───────────────────────────────────────── + + Future _getToken() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('access_token'); // ← ganti ini +} + + Future _getIdTeknisi() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getInt('id_teknisi'); + } + + Future> _getHeaders() async { + final token = await _getToken(); + return { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + if (token != null) 'Authorization': 'Bearer $token', + }; + } + + Map _handleResponse(http.Response response) { + // Log response di mode debug + if (const bool.fromEnvironment('dart.vm.product') == false) { + print('[PenugasanApi] ${response.request?.method} ${response.request?.url}'); + print('[PenugasanApi] Status: ${response.statusCode}'); + if (response.statusCode >= 400) { + print('[PenugasanApi] Error Body: ${response.body}'); + } + } + + final data = json.decode(response.body); + if (response.statusCode >= 200 && response.statusCode < 300) { + return data; + } + throw PenugasanApiException( + message: data['message'] ?? 'Terjadi kesalahan', + statusCode: response.statusCode, + errors: data['errors'], + ); + } + + // ───────────────────────────────────────── + // GET - Daftar Penugasan + // ───────────────────────────────────────── + /// [status] filter: 'belum_mulai' | 'dalam_proses' | 'selesai' | 'dibatalkan' + /// + /// Untuk tab "Progres Aktif" → kirim status: 'dalam_proses' + /// Untuk tab "Riwayat Selesai" → kirim status: 'selesai' + /// Tanpa status → ambil semua + Future> getPenugasanList({ + String? status, + String? jenisPekerjaan, + String? tanggalMulai, + String? tanggalAkhir, + int page = 1, + }) async { + final idTeknisi = await _getIdTeknisi(); + + final queryParams = { + 'page': page.toString(), + if (idTeknisi != null) 'id_teknisi': idTeknisi.toString(), + }; + + if (status != null) queryParams['status'] = status; + if (jenisPekerjaan != null) queryParams['jenis_pekerjaan'] = jenisPekerjaan; + if (tanggalMulai != null) queryParams['tanggal_mulai'] = tanggalMulai; + if (tanggalAkhir != null) queryParams['tanggal_akhir'] = tanggalAkhir; + + final uri = Uri.parse('$baseUrl/penugasan') + .replace(queryParameters: queryParams); + + final response = await http.get(uri, headers: await _getHeaders()); + return _handleResponse(response); + } + + // ───────────────────────────────────────── + // GET - Helper: Progres Aktif + // ───────────────────────────────────────── + /// Shortcut untuk mengambil penugasan dengan status 'dalam_proses' + Future> getPenugasanProgresAktif({int page = 1}) async { + return getPenugasanList(status: 'dalam_proses', page: page); + } + + // ───────────────────────────────────────── + // GET - Helper: Riwayat Selesai + // ───────────────────────────────────────── + /// Shortcut untuk mengambil penugasan dengan status 'selesai' + Future> getPenugasanRiwayatSelesai({int page = 1}) async { + return getPenugasanList(status: 'selesai', page: page); + } + + // ───────────────────────────────────────── + // GET - Detail Penugasan + // ───────────────────────────────────────── + Future> getPenugasanDetail(int id) async { + final response = await http.get( + Uri.parse('$baseUrl/penugasan/$id'), + headers: await _getHeaders(), + ); + return _handleResponse(response); + } + + // ───────────────────────────────────────── + // GET - Statistik + // ───────────────────────────────────────── + Future> getStatistik() async { + final idTeknisi = await _getIdTeknisi(); + + final uri = Uri.parse('$baseUrl/penugasan/statistik').replace( + queryParameters: { + if (idTeknisi != null) 'id_teknisi': idTeknisi.toString(), + }, + ); + + final response = await http.get(uri, headers: await _getHeaders()); + return _handleResponse(response); + } + + // ───────────────────────────────────────── + // POST - Lengkapi Detail (pertama kali) + // ───────────────────────────────────────── + Future> lengkapiDetail({ + required int idPenugasan, + required List> items, + required String tanggalMulai, + String? detailPekerjaan, + List? timTeknisi, + XFile? fotoSebelum, + XFile? fotoSesudah, + }) async { + final token = await _getToken(); + var request = http.MultipartRequest( + 'POST', + Uri.parse('$baseUrl/penugasan/$idPenugasan/lengkapi-detail'), + ); + + request.headers.addAll({ + 'Accept': 'application/json', + if (token != null) 'Authorization': 'Bearer $token', + }); + + request.fields['tanggal_mulai'] = tanggalMulai; + + // Selalu kirim detail_pekerjaan (meski kosong) + // agar Laravel menyimpan nilai terbaru via $request->has() + request.fields['detail_pekerjaan'] = detailPekerjaan ?? ''; + + for (int i = 0; i < items.length; i++) { + final item = items[i]; + request.fields['items[$i][jenis_pekerjaan]'] = + item['jenis_pekerjaan']?.toString() ?? ''; + + if (item['dimensi_pipa'] != null) + request.fields['items[$i][dimensi_pipa]'] = + item['dimensi_pipa'].toString(); + + if (item['jarak_meter'] != null) + request.fields['items[$i][jarak_meter]'] = + item['jarak_meter'].toString(); + + if (item['jumlah_unit'] != null) + request.fields['items[$i][jumlah_unit]'] = + item['jumlah_unit'].toString(); + + if (item['jumlah_titik'] != null) + request.fields['items[$i][jumlah_titik]'] = + item['jumlah_titik'].toString(); + + if (item['pakai_pipa_besi'] != null) + request.fields['items[$i][pakai_pipa_besi]'] = + item['pakai_pipa_besi'] ? '1' : '0'; + + if (item['jenis_pengangkatan'] != null) + request.fields['items[$i][jenis_pengangkatan]'] = + item['jenis_pengangkatan'].toString(); + } + + if (timTeknisi != null) { + for (int i = 0; i < timTeknisi.length; i++) { + request.fields['tim_teknisi[$i]'] = timTeknisi[i].toString(); + } + } + + // ✅ Upload foto sebelum (jika ada) + if (fotoSebelum != null) { + final bytes = await fotoSebelum.readAsBytes(); + request.fields['foto_sebelum_base64'] = + 'data:image/jpeg;base64,' + base64Encode(bytes); + } + + // ✅ Upload foto sesudah (jika ada) + if (fotoSesudah != null) { + final bytes = await fotoSesudah.readAsBytes(); + request.fields['foto_sesudah_base64'] = + 'data:image/jpeg;base64,' + base64Encode(bytes); + } + + final streamed = await request.send(); + final response = await http.Response.fromStream(streamed); + return _handleResponse(response); + } + + // ───────────────────────────────────────── + // PUT - Update Detail (edit yang sudah ada) + // ───────────────────────────────────────── + Future> updateDetail({ + required int idPenugasan, + required int idTeknisi, + required List> items, + // Selalu kirim detail_pekerjaan (default string kosong, bukan null) + String detailPekerjaan = '', + String? tanggalMulai, + List? timTeknisi, + }) async { + // Bersihkan nilai null di dalam setiap item sebelum encode JSON + final cleanedItems = items.map((item) { + final cleaned = {}; + item.forEach((key, value) { + // id_penugasan_item & jenis_pekerjaan selalu dikirim meski null + if (key == 'id_penugasan_item' || key == 'jenis_pekerjaan') { + cleaned[key] = value; + } else if (value != null) { + cleaned[key] = value; + } + }); + return cleaned; + }).toList(); + + final body = { + 'id_teknisi': idTeknisi, + 'items': cleanedItems, + // Selalu kirim detail_pekerjaan agar backend menyimpan nilai terbaru + 'detail_pekerjaan': detailPekerjaan, + if (tanggalMulai != null) 'tanggal_mulai': tanggalMulai, + if (timTeknisi != null) 'tim_teknisi': timTeknisi, + }; + + final response = await http.put( + Uri.parse('$baseUrl/penugasan/$idPenugasan/update-detail'), + headers: await _getHeaders(), + body: json.encode(body), + ); + return _handleResponse(response); + } + + // ───────────────────────────────────────── + // POST - Tambah Item Progres + // ───────────────────────────────────────── + Future> addItem({ + required int idPenugasan, + required Map item, + }) async { + final response = await http.post( + Uri.parse('$baseUrl/penugasan/$idPenugasan/add-item'), + headers: await _getHeaders(), + body: json.encode(item), + ); + return _handleResponse(response); + } + + // ───────────────────────────────────────── + // PUT - Update Status + // ───────────────────────────────────────── + Future> updateStatus({ + required int idPenugasan, + required String statusPekerjaan, + String? tanggalDiselesaikan, + }) async { + final response = await http.put( + Uri.parse('$baseUrl/penugasan/$idPenugasan/update-status'), + headers: await _getHeaders(), + body: json.encode({ + 'status_pekerjaan': statusPekerjaan, + if (tanggalDiselesaikan != null) + 'tanggal_diselesaikan': tanggalDiselesaikan, + }), + ); + return _handleResponse(response); + } + + // ───────────────────────────────────────── + // POST - Upload Foto Sebelum / Sesudah + // ───────────────────────────────────────── + /// [tipeFoto] : 'sebelum' atau 'sesudah' + /// + /// Field yang dikirim ke backend: + /// - tipe_foto : 'sebelum' | 'sesudah' ← wajib (required di Laravel) + /// - sebelum_base64 : base64 string ← jika tipeFoto == 'sebelum' + /// - sesudah_base64 : base64 string ← jika tipeFoto == 'sesudah' + Future> uploadFoto({ + required int idPenugasan, + required String tipeFoto, // 'sebelum' atau 'sesudah' + required XFile foto, + }) async { + assert( + tipeFoto == 'sebelum' || tipeFoto == 'sesudah', + 'tipeFoto harus "sebelum" atau "sesudah"', + ); + + final token = await _getToken(); + var request = http.MultipartRequest( + 'POST', + Uri.parse('$baseUrl/penugasan/$idPenugasan/upload-foto'), + ); + + request.headers.addAll({ + 'Accept': 'application/json', + if (token != null) 'Authorization': 'Bearer $token', + }); + + // ✅ Field wajib: tipe_foto — dipakai backend untuk validasi & routing + request.fields['tipe_foto'] = tipeFoto; + + // ✅ Field base64 dinamis sesuai tipe: + // tipeFoto='sebelum' → key: 'sebelum_base64' + // tipeFoto='sesudah' → key: 'sesudah_base64' + final bytes = await foto.readAsBytes(); + request.fields['${tipeFoto}_base64'] = + 'data:image/jpeg;base64,' + base64Encode(bytes); + + final streamed = await request.send(); + final response = await http.Response.fromStream(streamed); + return _handleResponse(response); + } + + // ───────────────────────────────────────── + // GET - Tarif by Jenis Pekerjaan + // ───────────────────────────────────────── + Future> getTarifByJenis(String jenisPekerjaan) async { + final response = await http.get( + Uri.parse( + '$baseUrl/penugasan/master/tarif-by-jenis?jenis_pekerjaan=$jenisPekerjaan'), + headers: await _getHeaders(), + ); + return _handleResponse(response); + } + + // ───────────────────────────────────────── + // GET - List Teknisi + // ───────────────────────────────────────── + Future> getTeknisiList() async { + final response = await http.get( + Uri.parse('$baseUrl/penugasan/master/teknisi-list'), + headers: await _getHeaders(), + ); + return _handleResponse(response); + } +} + +// =================================== +// EXCEPTION +// =================================== +class PenugasanApiException implements Exception { + final String message; + final int? statusCode; + final Map? errors; + + PenugasanApiException({ + required this.message, + this.statusCode, + this.errors, + }); + + @override + String toString() => + 'PenugasanApiException: $message (Status: $statusCode)'; + + /// Ambil pesan error pertama dari field validation errors + String? getFirstError() { + if (errors == null) return null; + for (var key in errors!.keys) { + final val = errors![key]; + if (val is List && val.isNotEmpty) return val.first.toString(); + } + return null; + } +} \ No newline at end of file diff --git a/samooflutter/lib/keuangan/FinanceMain.dart b/samooflutter/lib/keuangan/FinanceMain.dart new file mode 100644 index 0000000..1f0146a --- /dev/null +++ b/samooflutter/lib/keuangan/FinanceMain.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'Gaji.dart'; +import 'Kasbon.dart'; + +const _bg = Color(0xFFF9FAFB); +const _bg1 = Color(0xFFFFFFFF); +const _amber = Color(0xFFF59E0B); +const _rose = Color(0xFFEF4444); +const _t1 = Color(0xFF111827); +const _t2 = Color(0xFF6B7280); + +class FinanceMainScreen extends StatefulWidget { + const FinanceMainScreen({super.key}); + + @override + State createState() => _FinanceMainScreenState(); +} + +class _FinanceMainScreenState extends State with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: _bg, + appBar: AppBar( + elevation: 0, + backgroundColor: _bg1, + title: const Text('Keuangan Teknisi', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w800, color: _t1)), + bottom: TabBar( + controller: _tabController, + indicatorColor: _amber, + indicatorWeight: 3, + labelColor: _t1, + unselectedLabelColor: _t2, + labelStyle: const TextStyle(fontWeight: FontWeight.w800, fontSize: 13, letterSpacing: 0.5), + tabs: const [ + Tab( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.payments_rounded, size: 18), + SizedBox(width: 8), + Text('RIWAYAT GAJI'), + ], + ), + ), + Tab( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.account_balance_wallet_rounded, size: 18), + SizedBox(width: 8), + Text('RINCIAN KASBON'), + ], + ), + ), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: const [ + GajiRiwayatScreen(), + KasbonRiwayatScreen(), + ], + ), + ); + } +} diff --git a/samooflutter/lib/keuangan/Gaji.dart b/samooflutter/lib/keuangan/Gaji.dart new file mode 100644 index 0000000..3b7148a --- /dev/null +++ b/samooflutter/lib/keuangan/Gaji.dart @@ -0,0 +1,336 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import '../api/GajiApi.dart'; + +const _bg = Color(0xFFF9FAFB); +const _bg1 = Color(0xFFFFFFFF); +const _bg2 = Color(0xFFF3F4F6); +const _green = Color(0xFF10B981); +const _greenDim = Color(0x1A10B981); +const _cyan = Color(0xFF06B6D4); +const _cyanDim = Color(0x1A06B6D4); +const _amber = Color(0xFFF59E0B); +const _amberDim = Color(0x1AF59E0B); +const _rose = Color(0xFFEF4444); +const _roseDim = Color(0x1AEF4444); +const _t1 = Color(0xFF111827); +const _t2 = Color(0xFF6B7280); +const _t3 = Color(0xFF9CA3AF); +const _line2 = Color(0xFFE5E7EB); + +class GajiRiwayatScreen extends StatefulWidget { + const GajiRiwayatScreen({super.key}); + + @override + State createState() => _GajiRiwayatScreenState(); +} + +class _GajiRiwayatScreenState extends State { + final GajiApi _apiService = GajiApi(); + List _riwayatGaji = []; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _fetchRiwayat(); + } + + Future _fetchRiwayat() async { + setState(() => _isLoading = true); + final res = await _apiService.getRiwayat(); + if (mounted) { + setState(() { + _isLoading = false; + if (res['success'] == true) { + // Laravel paginate structure is res['data']['data'] + final rawData = res['data']; + if (rawData is Map && rawData.containsKey('data')) { + _riwayatGaji = rawData['data'] ?? []; + } else { + _riwayatGaji = rawData ?? []; + } + } + }); + } + } + + String _formatCurrency(dynamic amount) { + final formatter = NumberFormat.currency(locale: 'id_ID', symbol: 'Rp ', decimalDigits: 0); + return formatter.format(amount ?? 0); + } + + @override + Widget build(BuildContext context) { + return _isLoading + ? const Center(child: CircularProgressIndicator(color: _amber)) + : RefreshIndicator( + onRefresh: _fetchRiwayat, + color: _amber, + backgroundColor: _bg1, + child: _riwayatGaji.isEmpty + ? _buildEmptyState() + : ListView.builder( + padding: const EdgeInsets.all(20), + itemCount: _riwayatGaji.length, + itemBuilder: (context, i) => _buildGajiCard(_riwayatGaji[i]), + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: _bg1, + shape: BoxShape.circle, + border: Border.all(color: _line2), + ), + child: const Icon(Icons.receipt_long_rounded, size: 40, color: _t3), + ), + const SizedBox(height: 16), + const Text('Belum Ada Riwayat', + style: TextStyle(color: _t1, fontSize: 16, fontWeight: FontWeight.w700)), + const SizedBox(height: 4), + const Text('Slip gaji Anda akan muncul di sini', + style: TextStyle(color: _t2, fontSize: 13)), + ], + ), + ); + } + + Widget _buildGajiCard(dynamic item) { + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: _bg1, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: _line2), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _showGajiDetail(item['id_penggajian']), + borderRadius: BorderRadius.circular(16), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Container( + width: 48, height: 48, + decoration: BoxDecoration( + color: _amberDim, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: _amber.withOpacity(0.3)), + ), + child: const Icon(Icons.receipt_long_rounded, color: _amber, size: 22), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Periode: ${item['nama_bulan'] ?? ''} ${item['periode_tahun'] ?? ''}', + style: const TextStyle( + color: _t1, + fontWeight: FontWeight.w700, + fontSize: 14)), + const SizedBox(height: 4), + Text('Dibayar: ${item['tanggal_bayar'] ?? '-'}', + style: const TextStyle(color: _t2, fontSize: 11)), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(_formatCurrency(item['gaji_bersih']), + style: const TextStyle( + color: _green, + fontWeight: FontWeight.w800, + fontSize: 15)), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: (item['is_paid'] ?? false) ? _greenDim : _amberDim, + borderRadius: BorderRadius.circular(6), + ), + child: Text((item['is_paid'] ?? false) ? 'LUNAS' : 'BELUM DIBAYAR', + style: TextStyle( + color: (item['is_paid'] ?? false) ? _green : _amber, + fontSize: 9, + fontWeight: FontWeight.w900, + letterSpacing: 0.5)), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } + + void _showGajiDetail(int id) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => _GajiDetailSheet(id: id, apiService: _apiService), + ); + } +} + +class _GajiDetailSheet extends StatefulWidget { + final int id; + final GajiApi apiService; + const _GajiDetailSheet({required this.id, required this.apiService}); + + @override + State<_GajiDetailSheet> createState() => _GajiDetailSheetState(); +} + +class _GajiDetailSheetState extends State<_GajiDetailSheet> { + bool _isLoading = true; + dynamic _detail; + + @override + void initState() { + super.initState(); + _fetchDetail(); + } + + Future _fetchDetail() async { + final res = await widget.apiService.getDetail(widget.id); + if (mounted) { + setState(() { + _detail = res['data']?['header']; // We use 'header' for top summary + _isLoading = false; + }); + } + } + + String _formatCurrency(dynamic amount) { + final formatter = NumberFormat.currency(locale: 'id_ID', symbol: 'Rp ', decimalDigits: 0); + return formatter.format(amount ?? 0); + } + + @override + Widget build(BuildContext context) { + return DraggableScrollableSheet( + initialChildSize: 0.8, + maxChildSize: 0.95, + minChildSize: 0.5, + builder: (_, controller) => Container( + decoration: const BoxDecoration( + color: _bg1, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: _isLoading + ? const Center(child: CircularProgressIndicator(color: _amber)) + : Column( + children: [ + const SizedBox(height: 12), + Container(width: 40, height: 4, decoration: BoxDecoration(color: _line2, borderRadius: BorderRadius.circular(2))), + const SizedBox(height: 24), + Expanded( + child: ListView( + controller: controller, + padding: const EdgeInsets.symmetric(horizontal: 24), + children: [ + Center( + child: Column( + children: [ + const Text('SLIP GAJI TEKNISI', + style: TextStyle(color: _t2, fontSize: 11, fontWeight: FontWeight.w800, letterSpacing: 2)), + const SizedBox(height: 8), + Text(_detail['periode'] ?? '-', + style: const TextStyle(color: _t1, fontSize: 18, fontWeight: FontWeight.w800)), + const SizedBox(height: 24), + ], + ), + ), + + _buildInfoSection('RINCIAN PENDAPATAN', [ + _buildDetailRow('Gaji Pokok', _formatCurrency(_detail['gaji_pokok'] ?? 0)), + _buildDetailRow('Uang Makan', _formatCurrency(_detail['potongan_makan'] ?? 0)), // It's usually a static or dynamic field + _buildDetailRow('Insentif Pekerjaan', _formatCurrency(_detail['gaji_kotor'] ?? 0), isBold: true), + ]), + + const SizedBox(height: 20), + _buildInfoSection('POTONGAN', [ + _buildDetailRow('Potongan Kasbon', '- ${_formatCurrency(_detail['potongan_kasbon'])}', color: _rose), + _buildDetailRow('Potongan Absensi', '- ${_formatCurrency(_detail['potongan_absensi'])}', color: _rose), + ]), + + const Divider(height: 48, color: _line2), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('TOTAL DITERIMA', + style: TextStyle(color: _t1, fontSize: 14, fontWeight: FontWeight.w800)), + Text(_formatCurrency(_detail['gaji_bersih']), + style: const TextStyle(color: _green, fontSize: 22, fontWeight: FontWeight.w900)), + ], + ), + + const SizedBox(height: 32), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _bg2, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: _line2), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('CATATAN PEKERJAAN', + style: TextStyle(color: _t2, fontSize: 10, fontWeight: FontWeight.w800, letterSpacing: 1)), + const SizedBox(height: 8), + Text(_detail['rincian_pekerjaan'] ?? 'Tidak ada rincian', + style: const TextStyle(color: _t1, fontSize: 12, height: 1.5)), + ], + ), + ), + const SizedBox(height: 40), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildInfoSection(String title, List children) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: const TextStyle(color: _t3, fontSize: 10, fontWeight: FontWeight.w900, letterSpacing: 1.2)), + const SizedBox(height: 12), + ...children, + ], + ); + } + + Widget _buildDetailRow(String label, String value, {bool isBold = false, Color? color}) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: TextStyle(color: _t2, fontSize: 13, fontWeight: isBold ? FontWeight.w700 : FontWeight.w400)), + Text(value, style: TextStyle(color: color ?? _t1, fontSize: 13, fontWeight: isBold ? FontWeight.w800 : FontWeight.w600)), + ], + ), + ); + } +} diff --git a/samooflutter/lib/keuangan/Kasbon.dart b/samooflutter/lib/keuangan/Kasbon.dart new file mode 100644 index 0000000..a28e886 --- /dev/null +++ b/samooflutter/lib/keuangan/Kasbon.dart @@ -0,0 +1,260 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import '../api/KasbonApi.dart'; + +const _bg = Color(0xFFF9FAFB); +const _bg1 = Color(0xFFFFFFFF); +const _bg2 = Color(0xFFF3F4F6); +const _green = Color(0xFF10B981); +const _greenDim = Color(0x1A10B981); +const _cyan = Color(0xFF06B6D4); +const _cyanDim = Color(0x1A06B6D4); +const _amber = Color(0xFFF59E0B); +const _amberDim = Color(0x1AF59E0B); +const _rose = Color(0xFFEF4444); +const _roseDim = Color(0x1AEF4444); +const _t1 = Color(0xFF111827); +const _t2 = Color(0xFF6B7280); +const _t3 = Color(0xFF9CA3AF); +const _line2 = Color(0xFFE5E7EB); + +class KasbonRiwayatScreen extends StatefulWidget { + const KasbonRiwayatScreen({super.key}); + + @override + State createState() => _KasbonRiwayatScreenState(); +} + +class _KasbonRiwayatScreenState extends State { + final KasbonApi _apiService = KasbonApi(); + List _riwayat = []; + Map? _statistik; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _fetchData(); + } + + Future _fetchData() async { + setState(() => _isLoading = true); + try { + final results = await Future.wait([ + _apiService.getRiwayat(), + _fetchStatistik(), + ]); + + if (mounted) { + setState(() { + final resRiwayat = results[0] as Map?; + final resStat = results[1] as Map?; + + if (resRiwayat != null && resRiwayat['success'] == true) { + // Laravel paginate structure + final rawData = resRiwayat['data']; + if (rawData is Map && rawData.containsKey('data')) { + _riwayat = rawData['data'] ?? []; + } else { + _riwayat = rawData ?? []; + } + } + if (resStat != null) { + _statistik = resStat; + } + _isLoading = false; + }); + } + } catch (e) { + if (mounted) setState(() => _isLoading = false); + } + } + + Future?> _fetchStatistik() async { + // API Route: /kasbon/statistik + try { + final response = await _apiService.getStatistik(); // Use the new method + return response['data']; + } catch (e) { return null; } + } + + String _formatCurrency(dynamic amount) { + final formatter = NumberFormat.currency(locale: 'id_ID', symbol: 'Rp ', decimalDigits: 0); + return formatter.format(amount ?? 0); + } + + @override + Widget build(BuildContext context) { + return _isLoading + ? const Center(child: CircularProgressIndicator(color: _rose)) + : RefreshIndicator( + onRefresh: _fetchData, + color: _rose, + backgroundColor: _bg1, + child: CustomScrollView( + slivers: [ + // STATS SECTION + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('RINGKASAN KASBON', + style: TextStyle(color: _t3, fontSize: 10, fontWeight: FontWeight.w900, letterSpacing: 1.5)), + const SizedBox(height: 12), + _buildTotalKasbonCard(), + ], + ), + ), + ), + + // LIST SECTION + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('RIWAYAT PINJAMAN', + style: TextStyle(color: _t3, fontSize: 10, fontWeight: FontWeight.w900, letterSpacing: 1.5)), + Text('${_riwayat.length} Transaksi', + style: const TextStyle(color: _t2, fontSize: 10)), + ], + ), + ), + ), + + const SliverPadding(padding: EdgeInsets.only(top: 12)), + + _riwayat.isEmpty + ? SliverToBoxAdapter(child: _buildEmptyState()) + : SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: _buildKasbonCard(_riwayat[i]), + ), + childCount: _riwayat.length, + ), + ), + + const SliverPadding(padding: EdgeInsets.only(bottom: 32)), + ], + ), + ); + } + + Widget _buildTotalKasbonCard() { + final outstanding = _statistik?['total_hutang'] ?? _statistik?['outstanding_kasbon'] ?? 0; + return Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [_rose, _rose.withOpacity(0.7)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow(color: _rose.withOpacity(0.3), blurRadius: 15, offset: const Offset(0, 8)), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Sisa Hutang Kasbon', + style: TextStyle(color: Colors.white70, fontSize: 12, fontWeight: FontWeight.w500)), + const SizedBox(height: 6), + Text(_formatCurrency(outstanding), + style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.w900, letterSpacing: -0.5)), + ], + ), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(14), + ), + child: const Icon(Icons.money_off_rounded, color: Colors.white, size: 28), + ), + ], + ), + ); + } + + Widget _buildEmptyState() { + return Padding( + padding: const EdgeInsets.only(top: 40), + child: Center( + child: Column( + children: [ + Icon(Icons.history_rounded, size: 48, color: _t3), + const SizedBox(height: 12), + const Text('Belum ada riwayat kasbon', style: TextStyle(color: _t2, fontSize: 13)), + ], + ), + ), + ); + } + + Widget _buildKasbonCard(dynamic item) { + final bool isLunas = item['status'] == 'Lunas'; + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _bg1, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: _line2), + ), + child: Row( + children: [ + Container( + width: 44, height: 44, + decoration: BoxDecoration( + color: isLunas ? _greenDim : _roseDim, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: (isLunas ? _green : _rose).withOpacity(0.3)), + ), + child: Icon(isLunas ? Icons.check_circle_rounded : Icons.pending_rounded, + color: isLunas ? _green : _rose, size: 20), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(_formatCurrency(item['nominal']), + style: const TextStyle(color: _t1, fontWeight: FontWeight.w800, fontSize: 15)), + const SizedBox(height: 4), + Text(item['keperluan'] ?? 'Tanpa keterangan', + style: const TextStyle(color: _t2, fontSize: 12), maxLines: 1, overflow: TextOverflow.ellipsis), + const SizedBox(height: 4), + Text(item['tanggal'] ?? '-', + style: const TextStyle(color: _t3, fontSize: 10)), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: isLunas ? _greenDim : _roseDim, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: (isLunas ? _green : _rose).withOpacity(0.25)), + ), + child: Text(item['status_label']?.toUpperCase() ?? '-', + style: TextStyle( + color: isLunas ? _green : _rose, + fontSize: 10, + fontWeight: FontWeight.w900, + letterSpacing: 0.5)), + ), + ], + ), + ); + } +} diff --git a/samooflutter/lib/login/login.dart b/samooflutter/lib/login/login.dart new file mode 100644 index 0000000..0db7b4e --- /dev/null +++ b/samooflutter/lib/login/login.dart @@ -0,0 +1,218 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import '../api/LoginApi.dart'; + +const _bg = Color(0xFFF9FAFB); +const _bg1 = Color(0xFFFFFFFF); +const _green = Color(0xFF10B981); +const _greenDim = Color(0x1A2bff88); +const _t1 = Color(0xFF111827); +const _t2 = Color(0xFF6B7280); +const _t3 = Color(0xFF9CA3AF); +const _line2 = Color(0xFFE5E7EB); + +class LoginScreen extends StatefulWidget { + const LoginScreen({super.key}); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + final _usernameController = TextEditingController(); + final _passwordController = TextEditingController(); + bool _isPasswordVisible = false; + bool _isLoading = false; + final _apiService = ApiService(); + + Future _handleLogin() async { + if (_usernameController.text.isEmpty || _passwordController.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Username dan Password harus diisi')), + ); + return; + } + + setState(() => _isLoading = true); + + try { + final response = await _apiService.login( + _usernameController.text, + _passwordController.text, + ); + + if (response['success']) { + Navigator.pushReplacementNamed(context, '/dashboard'); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(response['message'] ?? 'Login gagal')), + ); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Terjadi kesalahan koneksi')), + ); + } finally { + setState(() => _isLoading = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: _bg, + body: AnnotatedRegion( + value: SystemUiOverlayStyle.light, + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 30), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 100), + + // LOGO BARU (NATURAL & PROFESIONAL) + Center( + child: Container( + height: 120, width: 120, + decoration: BoxDecoration( + color: _bg1, + borderRadius: BorderRadius.circular(30), + boxShadow: [BoxShadow(color: _green.withOpacity(0.1), blurRadius: 40)], + ), + child: Center( + child: Icon(Icons.water_drop_rounded, color: _green, size: 70), // Fallback icon while assets loading + ), + ), + ), + + const SizedBox(height: 32), + Center( + child: Column( + children: [ + RichText( + text: const TextSpan( + children: [ + TextSpan(text: 'PDAM ', style: TextStyle(fontSize: 28, fontWeight: FontWeight.w900, color: _t1)), + TextSpan(text: 'Teknisi', style: TextStyle(fontSize: 28, fontWeight: FontWeight.w900, color: _green)), + ], + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration(color: _bg1, borderRadius: BorderRadius.circular(100), border: Border.all(color: _line2)), + child: const Text('Sistem Manajemen Teknisi', style: TextStyle(color: _t2, fontSize: 11, fontWeight: FontWeight.w600)), + ), + ], + ), + ), + + const SizedBox(height: 60), + + // USERNAME + _label('USERNAME', Icons.person_outline_rounded), + TextField( + controller: _usernameController, + style: const TextStyle(color: _t1), + decoration: _inputStyle('Masukkan username'), + ), + + const SizedBox(height: 24), + + // PASSWORD + _label('PASSWORD', Icons.lock_outline_rounded), + TextField( + controller: _passwordController, + obscureText: !_isPasswordVisible, + style: const TextStyle(color: _t1), + decoration: _inputStyle('Masukkan password').copyWith( + suffixIcon: IconButton( + icon: Icon(_isPasswordVisible ? Icons.visibility_off : Icons.visibility, color: _t3, size: 20), + onPressed: () => setState(() => _isPasswordVisible = !_isPasswordVisible), + ), + ), + ), + + const SizedBox(height: 12), + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () {}, + child: const Text('Lupa password?', style: TextStyle(color: _t2, fontSize: 12, fontWeight: FontWeight.w600)), + ), + ), + + const SizedBox(height: 32), + + // BUTTON MASUK + SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + onPressed: _isLoading ? null : _handleLogin, + style: ElevatedButton.styleFrom( + backgroundColor: _green, + foregroundColor: Colors.black, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), + elevation: 10, + shadowColor: _green.withOpacity(0.4), + ), + child: _isLoading + ? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(color: Colors.black, strokeWidth: 3)) + : const Text('MASUK', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 1)), + ), + ), + + const SizedBox(height: 40), + + // INFO + const Divider(color: _line2), + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration(color: _bg1, borderRadius: BorderRadius.circular(20), border: Border.all(color: _line2)), + child: const Row( + children: [ + Icon(Icons.info_outline_rounded, color: _green, size: 20), + const SizedBox(width: 14), + Expanded( + child: Text('Butuh bantuan? Hubungi admin PDAM Anda untuk mendapatkan akses.', + style: TextStyle(color: _t2, fontSize: 11, height: 1.5, fontWeight: FontWeight.w500)), + ), + ], + ), + ), + const SizedBox(height: 40), + ], + ), + ), + ), + ); + } + + Widget _label(String text, IconData icon) { + return Padding( + padding: const EdgeInsets.only(left: 4, bottom: 8), + child: Row( + children: [ + Icon(icon, color: _green, size: 14), + const SizedBox(width: 8), + Text(text, style: const TextStyle(color: _t2, fontSize: 10, fontWeight: FontWeight.w800, letterSpacing: 1.5)), + ], + ), + ); + } + + InputDecoration _inputStyle(String hint) { + return InputDecoration( + hintText: hint, + hintStyle: const TextStyle(color: _t3, fontSize: 14), + filled: true, + fillColor: _bg1, + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(18), borderSide: const BorderSide(color: _line2)), + enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(18), borderSide: const BorderSide(color: _line2)), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(18), borderSide: const BorderSide(color: _green, width: 1.5)), + ); + } +} \ No newline at end of file diff --git a/samooflutter/lib/main.dart b/samooflutter/lib/main.dart new file mode 100644 index 0000000..05f6078 --- /dev/null +++ b/samooflutter/lib/main.dart @@ -0,0 +1,836 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:device_preview/device_preview.dart'; +import 'package:flutter/foundation.dart'; +import 'login/login.dart'; +import '../api/LoginApi.dart'; +import '../absensi/absensi.dart'; +import '../api/AbsensiApi.dart'; +import 'models/Absensi_model.dart'; +import 'tugas/Penugasan.dart'; +import 'tugas/Progress.dart'; +import '../api/PenugasanApi.dart'; +import 'models/Penugasan_model.dart'; +import 'keuangan/FinanceMain.dart'; +import 'profil/Profil.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:intl/intl.dart'; + +import 'package:intl/date_symbol_data_local.dart'; + +// ── Palette (konsisten dengan login screen) ────────────────────────────────── +const _bg = Color(0xFFF9FAFB); +const _bg1 = Color(0xFFFFFFFF); +const _bg2 = Color(0xFFF3F4F6); +const _green = Color(0xFF10B981); +const _greenDim = Color(0x1A10B981); +const _greenGlow = Color(0x4D10B981); +const _cyan = Color(0xFF06B6D4); +const _cyanDim = Color(0x1A06B6D4); +const _amber = Color(0xFFF59E0B); +const _amberDim = Color(0x1AF59E0B); +const _rose = Color(0xFFEF4444); +const _roseDim = Color(0x1AEF4444); +const _violet = Color(0xFF8B5CF6); +const _violetDim = Color(0x1A8B5CF6); +const _t1 = Color(0xFF111827); +const _t2 = Color(0xFF6B7280); +const _t3 = Color(0xFF9CA3AF); +const _line2 = Color(0xFFE5E7EB); + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await initializeDateFormatting('id_ID', null); + runApp( + DevicePreview( + enabled: !kReleaseMode, + builder: (context) => const MyApp(), + ), + ); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + useInheritedMediaQuery: true, + builder: DevicePreview.appBuilder, + locale: DevicePreview.locale(context), + debugShowCheckedModeBanner: false, + title: 'PDAM Teknisi', + theme: ThemeData( + fontFamily: 'Roboto', + brightness: Brightness.light, + scaffoldBackgroundColor: _bg, + primarySwatch: Colors.blue, + colorScheme: ColorScheme.fromSeed( + seedColor: _green, + brightness: Brightness.light, + primary: _green, + surface: _bg1, + background: _bg, + ), + useMaterial3: true, + ), + initialRoute: '/', + routes: { + '/': (context) => const LoginScreen(), + '/dashboard': (context) => const DashboardScreen(), + }, + ); + } +} + +class DashboardScreen extends StatefulWidget { + const DashboardScreen({super.key}); + + @override + State createState() => _DashboardScreenState(); +} + +class _DashboardScreenState extends State { + int _selectedIndex = 0; + final ApiService _apiService = ApiService(); + Map? _userData; + Map? _dashboardData; + bool _isLoadingProfile = false; + bool _isLoadingDashboard = false; + + @override + void initState() { + super.initState(); + _loadUserData(); + _loadDashboardData(); + } + + Future _loadUserData() async { + setState(() { _isLoadingProfile = true; }); + final localUserData = await _apiService.getUserData(); + if (localUserData != null) { + setState(() { _userData = localUserData; }); + } + final result = await _apiService.getProfile(); + setState(() { _isLoadingProfile = false; }); + if (result['success'] == true && result['data'] != null) { + setState(() { _userData = result['data']; }); + } else if (result['logout'] == true) { + if (mounted) { + Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false); + } + } + } + + Future _loadDashboardData() async { + setState(() { _isLoadingDashboard = true; }); + final result = await _apiService.getDashboardData(); + if (mounted) { + setState(() { + _isLoadingDashboard = false; + if (result['success'] == true) { + _dashboardData = result['data']; + } + }); + } + } + + List get _pages => [ + HomePage( + userData: _userData, + dashboardData: _dashboardData, + isLoading: _isLoadingProfile || _isLoadingDashboard, + onNavigate: _onItemTapped, + ), + const AbsensiScreen(), + PenugasanScreen(), + ProgressScreen(), + const FinanceMainScreen(), + ]; + + void _onItemTapped(int index) { + setState(() { + _selectedIndex = index; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: _bg, + body: _pages[_selectedIndex], + bottomNavigationBar: _buildBottomNav(), + ); + } + + Widget _buildBottomNav() { + final items = [ + {'icon': Icons.grid_view_rounded, 'label': 'Dashboard'}, + {'icon': Icons.fingerprint_rounded, 'label': 'Absensi'}, + {'icon': Icons.assignment_rounded, 'label': 'Tugas'}, + {'icon': Icons.trending_up_rounded, 'label': 'Progress'}, + {'icon': Icons.account_balance_wallet_rounded, 'label': 'Gaji'}, + ]; + + return Container( + decoration: const BoxDecoration( + color: _bg1, + border: Border(top: BorderSide(color: _line2, width: 1)), + ), + child: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: List.generate(items.length, (i) { + final active = i == _selectedIndex; + return GestureDetector( + onTap: () => _onItemTapped(i), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric( + horizontal: 14, vertical: 6), + decoration: active + ? BoxDecoration( + color: _greenDim, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _green.withOpacity(0.3)), + ) + : null, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + items[i]['icon'] as IconData, + size: 22, + color: active ? _green : _t3, + ), + const SizedBox(height: 3), + Text( + items[i]['label'] as String, + style: TextStyle( + fontSize: 10, + fontWeight: active + ? FontWeight.w700 : FontWeight.w400, + color: active ? _green : _t3, + letterSpacing: active ? 0.3 : 0, + ), + ), + ], + ), + ), + ); + }), + ), + ), + ), + ); + } +} + +class HomePage extends StatelessWidget { + final Map? userData; + final Map? dashboardData; + final bool isLoading; + final Function(int) onNavigate; + + const HomePage({ + super.key, + this.userData, + this.dashboardData, + this.isLoading = false, + required this.onNavigate, + }); + + Future _handleLogout(BuildContext context) async { + final confirmed = await showDialog( + context: context, + builder: (context) => Dialog( + backgroundColor: _bg1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: const BorderSide(color: _line2), + ), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 52, height: 52, + decoration: BoxDecoration( + color: _roseDim, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: _rose.withOpacity(0.3)), + ), + child: const Icon(Icons.logout_rounded, + color: _rose, size: 24), + ), + const SizedBox(height: 16), + const Text('Konfirmasi Logout', + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w700, + color: _t1)), + const SizedBox(height: 8), + const Text('Apakah Anda yakin ingin keluar?', + style: TextStyle(fontSize: 13, color: _t2), + textAlign: TextAlign.center), + const SizedBox(height: 24), + Row(children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.pop(context, false), + style: OutlinedButton.styleFrom( + foregroundColor: _t2, + side: const BorderSide(color: _line2), + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + ), + child: const Text('Batal'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () => Navigator.pop(context, true), + style: ElevatedButton.styleFrom( + backgroundColor: _rose, + foregroundColor: Colors.white, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + ), + child: const Text('Logout', + style: TextStyle(fontWeight: FontWeight.w700)), + ), + ), + ]), + ], + ), + ), + ), + ); + + if (confirmed == true && context.mounted) { + final apiService = ApiService(); + await apiService.logout(); + if (context.mounted) { + Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false); + } + } + } + + @override + Widget build(BuildContext context) { + final teknisi = userData?['teknisi']; + final namaTeknisi = (teknisi?['nama'] ?? 'Teknisi') as String; + final idTeknisi = teknisi?['id_teknisi'] ?? '-'; + final username = userData?['username'] ?? '-'; + final initial = namaTeknisi.isNotEmpty + ? namaTeknisi[0].toUpperCase() : 'T'; + + return Scaffold( + backgroundColor: _bg, + appBar: AppBar( + elevation: 0, + backgroundColor: _bg1, + title: Row(children: [ + Container( + width: 30, height: 30, + decoration: BoxDecoration( + color: _greenDim, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: _green.withOpacity(0.35)), + ), + child: const Icon(Icons.water_drop_rounded, + color: _green, size: 15), + ), + const SizedBox(width: 10), + const Text('PDAM Teknisi', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: _t1, + letterSpacing: 0.2)), + ]), + actions: [ + IconButton( + icon: const Icon(Icons.account_circle_rounded, color: _t2, size: 24), + onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ProfilScreen())), + ), + IconButton( + icon: const Icon(Icons.logout_rounded, color: _rose, size: 22), + onPressed: () => _handleLogout(context), + ), + const SizedBox(width: 8), + ], + ), + body: RefreshIndicator( + color: _green, + backgroundColor: _bg1, + onRefresh: () async { + final apiService = ApiService(); + await apiService.getProfile(); + }, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.fromLTRB(20, 20, 20, 32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // New Premium Header with Attendance Status + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Halo, $namaTeknisi!', + style: const TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: _t1, letterSpacing: -1)), + const SizedBox(height: 4), + Row( + children: [ + Container(width: 8, height: 8, decoration: const BoxDecoration(color: _green, shape: BoxShape.circle)), + const SizedBox(width: 8), + const Text('Online & Siap Bertugas', style: TextStyle(fontSize: 12, color: _t2, fontWeight: FontWeight.w600)), + ], + ), + ], + ), + ], + ), + + const SizedBox(height: 32), + + // Wallet & Earnings Section (Horizontal Scroll or Compact) + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: _bg1, + borderRadius: BorderRadius.circular(28), + border: Border.all(color: _line2), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.2), blurRadius: 20, offset: const Offset(0, 10))], + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('RINGKASAN PENDAPATAN', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w800, color: _t3, letterSpacing: 1.5)), + Icon(Icons.account_balance_wallet_rounded, color: _green.withOpacity(0.5), size: 18), + ], + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Estimasi Gaji', style: TextStyle(fontSize: 11, color: _t2, fontWeight: FontWeight.w600)), + const SizedBox(height: 4), + Text(_formatCurrency(dashboardData?['statistik']?['estimasi_gaji'] ?? 0), + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: _green)), + ], + ), + ), + Container(width: 1, height: 40, color: _line2), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Kasbon Aktif', style: TextStyle(fontSize: 11, color: _t2, fontWeight: FontWeight.w600)), + const SizedBox(height: 4), + Text(_formatCurrency(dashboardData?['statistik']?['total_kasbon'] ?? 0), + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: _rose)), + ], + ), + ), + ], + ), + ], + ), + ), + + const SizedBox(height: 28), + + // New Performance & Statistics Section + _sectionLabel('RINGKASAN KINERJA BULAN INI'), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: _bg1, + borderRadius: BorderRadius.circular(32), + border: Border.all(color: _line2), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.2), blurRadius: 30, offset: const Offset(0, 15))], + ), + child: Row( + children: [ + // Circular Progress + Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: 86, height: 86, + child: CircularProgressIndicator( + value: ((dashboardData?['statistik']?['kehadiran'] ?? 0) as num) / 100.0, + strokeWidth: 9, + backgroundColor: _green.withOpacity(0.08), + valueColor: const AlwaysStoppedAnimation(_green), + strokeCap: StrokeCap.round, + ), + ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('${dashboardData?['statistik']?['kehadiran'] ?? 0}%', style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w900, color: _t1, letterSpacing: -0.5)), + Text('Hadir', style: TextStyle(fontSize: 9, color: _green.withOpacity(0.6), fontWeight: FontWeight.w800, letterSpacing: 0.5)), + ], + ), + ], + ), + const SizedBox(width: 24), + // Stats Details + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _miniRow(Icons.check_circle_outline_rounded, _green, 'Tugas Selesai', '${dashboardData?['statistik']?['tugas_selesai'] ?? 0} Item'), + const SizedBox(height: 12), + _miniRow(Icons.timer_outlined, _amber, 'Jam Kerja', '${dashboardData?['statistik']?['jam_kerja'] ?? 0} Jam'), + const SizedBox(height: 12), + _miniRow(Icons.trending_up_rounded, _cyan, 'Efisiensi', '+${dashboardData?['statistik']?['efisiensi'] ?? 0}%'), + ], + ), + ), + ], + ), + ), + + const SizedBox(height: 28), + + // Quick Actions or Status + _sectionLabel('AKTIVITAS TERAKHIR'), + const SizedBox(height: 16), + _buildModernStatCard( + 'Gaji Terakhir Diterima', + _formatCurrency(dashboardData?['statistik']?['gaji_terakhir'] ?? 0), + Icons.history_rounded, _cyan, + isWide: true + ), + ], + ), + ), + ), + ); + } + + Widget _miniRow(IconData icon, Color color, String label, String value) { + return Row( + children: [ + Icon(icon, color: color, size: 14), + const SizedBox(width: 8), + Text(label, style: const TextStyle(fontSize: 11, color: _t2, fontWeight: FontWeight.w500)), + const Spacer(), + Text(value, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w800, color: _t1)), + ], + ); + } + + Widget _buildModernStatCard(String title, String value, IconData icon, Color color, {bool isWide = false}) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: _bg1, + borderRadius: BorderRadius.circular(24), + border: Border.all(color: _line2), + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 20, offset: const Offset(0, 10)) + ], + ), + child: isWide + ? Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(14)), + child: Icon(icon, color: color, size: 22), + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: _t3, letterSpacing: 0.5)), + const SizedBox(height: 4), + Text(value, style: TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: color)), + ], + ), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12)), + child: Icon(icon, color: color, size: 20), + ), + const SizedBox(height: 20), + Text(value, style: TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: color)), + const SizedBox(height: 4), + Text(title, style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: _t3)), + ], + ), + ); + } + + Widget _pill(String text, Color bg, Color fg) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: bg, + borderRadius: BorderRadius.circular(99), + border: Border.all(color: fg.withOpacity(0.25)), + ), + child: Text(text, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: fg)), + ); + } + + Widget _sectionLabel(String label) { + return Text(label, style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: _t3, letterSpacing: 1.4)); + } + + Widget _buildStatCard(String title, String value, IconData icon, Color color, Color dimColor) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _bg1, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: _line2), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 34, height: 34, + decoration: BoxDecoration( + color: dimColor, + borderRadius: BorderRadius.circular(9), + border: Border.all(color: color.withOpacity(0.25)), + ), + child: Icon(icon, color: color, size: 17), + ), + const SizedBox(height: 10), + Text(value, style: TextStyle(fontSize: 22, fontWeight: FontWeight.w800, color: color, letterSpacing: -0.5)), + const SizedBox(height: 2), + Text(title, style: const TextStyle(fontSize: 11, color: _t3, fontWeight: FontWeight.w500)), + ], + ), + ); + } + + Widget _buildMenuCard(String title, IconData icon, Color color, Color dimColor, VoidCallback onTap) { + return Material( + color: _bg1, borderRadius: BorderRadius.circular(14), + child: InkWell( + onTap: onTap, borderRadius: BorderRadius.circular(14), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), border: Border.all(color: _line2)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + width: 36, height: 36, + decoration: BoxDecoration(color: dimColor, borderRadius: BorderRadius.circular(10), border: Border.all(color: color.withOpacity(0.25))), + child: Icon(icon, color: color, size: 18), + ), + Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: _t1)), + ], + ), + ), + ), + ); + } + String _formatCurrency(dynamic amount) { + final formatter = NumberFormat.currency(locale: 'id_ID', symbol: 'Rp ', decimalDigits: 0); + return formatter.format(amount ?? 0); + } +} + +class SalaryHistoryPage extends StatefulWidget { + const SalaryHistoryPage({super.key}); + + @override + State createState() => _SalaryHistoryPageState(); +} + +class _SalaryHistoryPageState extends State { + final ApiService _apiService = ApiService(); + List _riwayatGaji = []; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _fetchRiwayat(); + } + + Future _fetchRiwayat() async { + setState(() => _isLoading = true); + final res = await _apiService.getGajiRiwayat(); + if (mounted) { + setState(() { + _isLoading = false; + if (res['success'] == true) { + _riwayatGaji = res['data'] ?? []; + } + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: _bg, + appBar: AppBar( + elevation: 0, backgroundColor: _bg1, + title: const Row(children: [ + Icon(Icons.account_balance_wallet_rounded, color: _amber, size: 18), + SizedBox(width: 8), + Text('Riwayat Gaji', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: _t1)), + ]), + actions: [ + IconButton(onPressed: _fetchRiwayat, icon: const Icon(Icons.refresh, color: _t2)), + ], + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator(color: _amber)) + : _riwayatGaji.isEmpty + ? const Center(child: Text('Belum ada riwayat gaji', style: TextStyle(color: _t2))) + : ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: _riwayatGaji.length, + itemBuilder: (context, i) { + final item = _riwayatGaji[i]; + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _bg1, borderRadius: BorderRadius.circular(16), + border: Border.all(color: _line2), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration(color: _amberDim, borderRadius: BorderRadius.circular(12)), + child: const Icon(Icons.receipt_long_rounded, color: _amber, size: 20), + ), + const SizedBox(width: 16), + Expanded( + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Periode: ${item['periode']}', style: const TextStyle(color: _t1, fontWeight: FontWeight.bold)), + Text('Tanggal Bayar: ${item['tanggal_bayar'] ?? '-'}', style: const TextStyle(color: _t2, fontSize: 12)), + ]), + ), + Text('Rp${item['gaji_bersih']}', style: const TextStyle(color: _green, fontWeight: FontWeight.bold, fontSize: 16)), + ], + ), + ); + }, + ), + ); + } +} + +class KasbonHistoryPage extends StatefulWidget { + const KasbonHistoryPage({super.key}); + + @override + State createState() => _KasbonHistoryPageState(); +} + +class _KasbonHistoryPageState extends State { + final ApiService _apiService = ApiService(); + List _riwayat = []; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _fetch(); + } + + Future _fetch() async { + setState(() => _isLoading = true); + final res = await _apiService.getKasbonRiwayat(); + if (mounted) { + setState(() { + _isLoading = false; + if (res['success'] == true) _riwayat = res['data'] ?? []; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: AppBar( + elevation: 0, backgroundColor: const Color(0xFFFFFFFF), + title: const Text('Riwayat Kasbon', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF111827))), + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator(color: Color(0xFFF59E0B))) + : _riwayat.isEmpty + ? const Center(child: Text('Belum ada riwayat kasbon', style: TextStyle(color: Color(0xFF6B7280)))) + : ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: _riwayat.length, + itemBuilder: (context, i) { + final item = _riwayat[i]; + final bool isLunas = item['status'] == 'Lunas'; + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration(color: const Color(0xFFFFFFFF), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFE5E7EB))), + child: Row(children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration(color: isLunas ? const Color(0x1A10B981) : const Color(0x1AEF4444), borderRadius: BorderRadius.circular(12)), + child: Icon(isLunas ? Icons.check_circle_rounded : Icons.pending_rounded, color: isLunas ? const Color(0xFF10B981) : const Color(0xFFEF4444), size: 20), + ), + const SizedBox(width: 16), + Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Rp${item['jumlah']}', style: const TextStyle(color: Color(0xFF111827), fontWeight: FontWeight.bold)), + Text(item['keterangan'] ?? 'Tanpa keterangan', style: const TextStyle(color: Color(0xFF6B7280), fontSize: 12)), + Text(item['tanggal'] ?? '-', style: const TextStyle(color: Color(0xFF9CA3AF), fontSize: 11)), + ])), + _statusBadge(item['status']), + ]), + ); + }, + ), + ); + } + + Widget _statusBadge(String status) { + final bool isLunas = status == 'Lunas'; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration(color: isLunas ? const Color(0x1A10B981) : const Color(0x1AEF4444), borderRadius: BorderRadius.circular(8), border: Border.all(color: isLunas ? const Color(0xFF10B981).withOpacity(0.3) : const Color(0xFFEF4444).withOpacity(0.3))), + child: Text(status, style: TextStyle(color: isLunas ? const Color(0xFF10B981) : const Color(0xFFEF4444), fontSize: 10, fontWeight: FontWeight.bold)), + ); + } +} \ No newline at end of file diff --git a/samooflutter/lib/models/Absensi_model.dart b/samooflutter/lib/models/Absensi_model.dart new file mode 100644 index 0000000..07eee2d --- /dev/null +++ b/samooflutter/lib/models/Absensi_model.dart @@ -0,0 +1,83 @@ +class AbsensiModel { + final int? id; + final int idTeknisi; + final String tanggal; + final String? jamMasuk; + final String? jamKeluar; + final String? fotoAbsenMasuk; + final String? fotoAbsenKeluar; + final String status; + final String? keterangan; + + AbsensiModel({ + this.id, + required this.idTeknisi, + required this.tanggal, + this.jamMasuk, + this.jamKeluar, + this.fotoAbsenMasuk, + this.fotoAbsenKeluar, + required this.status, + this.keterangan, + }); + + factory AbsensiModel.fromJson(Map json) { + return AbsensiModel( + id: json['id'] != null ? int.tryParse(json['id'].toString()) : null, + idTeknisi: int.tryParse(json['id_teknisi']?.toString() ?? '') ?? 0, + tanggal: json['tanggal'], + jamMasuk: json['jam_masuk'], + jamKeluar: json['jam_keluar'], + fotoAbsenMasuk: json['foto_absen_masuk'], + fotoAbsenKeluar: json['foto_absen_keluar'], + status: json['status'], + keterangan: json['keterangan'], + ); + } + + Map toJson() { + return { + 'id': id, + 'id_teknisi': idTeknisi, + 'tanggal': tanggal, + 'jam_masuk': jamMasuk, + 'jam_keluar': jamKeluar, + 'foto_absen_masuk': fotoAbsenMasuk, + 'foto_absen_keluar': fotoAbsenKeluar, + 'status': status, + 'keterangan': keterangan, + }; + } +} + +class KeteranganOption { + final String label; + bool isSelected; + + KeteranganOption({ + required this.label, + this.isSelected = false, + }); +} + +class AbsensiStatus { + final bool sudahAbsenMasuk; + final bool sudahAbsenKeluar; + final AbsensiModel? dataAbsensi; + + AbsensiStatus({ + required this.sudahAbsenMasuk, + required this.sudahAbsenKeluar, + this.dataAbsensi, + }); + + factory AbsensiStatus.fromJson(Map json) { + return AbsensiStatus( + sudahAbsenMasuk: json['sudah_absen_masuk'] ?? false, + sudahAbsenKeluar: json['sudah_absen_keluar'] ?? false, + dataAbsensi: json['data_absensi'] != null + ? AbsensiModel.fromJson(json['data_absensi']) + : null, + ); + } +} \ No newline at end of file diff --git a/samooflutter/lib/models/Penugasan_model.dart b/samooflutter/lib/models/Penugasan_model.dart new file mode 100644 index 0000000..4b13d2d --- /dev/null +++ b/samooflutter/lib/models/Penugasan_model.dart @@ -0,0 +1,388 @@ +class PenugasanModel { + final int idPenugasan; + final int idTeknisi; + final String? fotoSurat; // ✅ Ganti lokasi → foto surat + final String? fotoSuratUrl; // ✅ URL lengkap foto surat + final String tanggalDiberikan; + final String? catatanAdmin; + final String? alamatLokasi; + final String? namaPelanggan; + final String? noSambungan; + + // Detail diisi teknisi via mobile + final String? jenisPekerjaan; + final String? labelJenisPekerjaan; + final String? dimensiPipa; + final double? jarakMeter; + final int? jumlahUnit; + final int? jumlahTitik; + final bool? pakaiPipaBesi; + final String? jenisPengangkatan; + final String? detailPekerjaan; + + // Ongkos + final int? idTarif; + final double? totalNilaiPekerjaan; + + // Status + final String statusPekerjaan; + final String? tanggalMulai; + final String? tanggalDiselesaikan; + + // Garansi + final String? tanggalGaransiMulai; + final String? tanggalGaransiSelesai; + final String? catatanGaransi; + final bool? isGaransiAktif; + final int? sisaHariGaransi; + + // Foto bukti pekerjaan + final String? fotoSebelum; + final String? fotoSesudah; + final String? fotoSebelumUrl; + final String? fotoSesudahUrl; + + // Relasi + final TeknisiModel? teknisi; + final TarifModel? tarif; + final List? timTeknisi; + + PenugasanModel({ + required this.idPenugasan, + required this.idTeknisi, + this.fotoSurat, + this.fotoSuratUrl, + required this.tanggalDiberikan, + this.catatanAdmin, + this.alamatLokasi, + this.namaPelanggan, + this.noSambungan, + this.jenisPekerjaan, + this.labelJenisPekerjaan, + this.dimensiPipa, + this.jarakMeter, + this.jumlahUnit, + this.jumlahTitik, + this.pakaiPipaBesi, + this.jenisPengangkatan, + this.detailPekerjaan, + this.idTarif, + this.totalNilaiPekerjaan, + required this.statusPekerjaan, + this.tanggalMulai, + this.tanggalDiselesaikan, + this.tanggalGaransiMulai, + this.tanggalGaransiSelesai, + this.catatanGaransi, + this.isGaransiAktif, + this.sisaHariGaransi, + this.fotoSebelum, + this.fotoSesudah, + this.fotoSebelumUrl, + this.fotoSesudahUrl, + this.teknisi, + this.tarif, + this.timTeknisi, + }); + + factory PenugasanModel.fromJson(Map json) { + return PenugasanModel( + idPenugasan: int.tryParse(json['id_penugasan']?.toString() ?? '') ?? 0, + idTeknisi: int.tryParse(json['id_teknisi']?.toString() ?? '') ?? 0, + fotoSurat: json['foto_surat'], + fotoSuratUrl: json['foto_surat_url'], + tanggalDiberikan: json['tanggal_diberikan'] ?? '', + catatanAdmin: json['catatan_admin'], + alamatLokasi: json['alamat_lokasi'], + namaPelanggan: json['nama_pelanggan'], + noSambungan: json['no_sambungan'], + jenisPekerjaan: json['jenis_pekerjaan'], + labelJenisPekerjaan: json['label_jenis_pekerjaan'], + dimensiPipa: json['dimensi_pipa'], + jarakMeter: json['jarak_meter'] != null + ? double.tryParse(json['jarak_meter'].toString()) + : null, + jumlahUnit: json['jumlah_unit'] != null + ? int.tryParse(json['jumlah_unit'].toString()) + : null, + jumlahTitik: json['jumlah_titik'] != null + ? int.tryParse(json['jumlah_titik'].toString()) + : null, + pakaiPipaBesi: json['pakai_pipa_besi'] != null + ? json['pakai_pipa_besi'] == 1 || + json['pakai_pipa_besi'] == true || + json['pakai_pipa_besi'].toString() == '1' || + json['pakai_pipa_besi'].toString() == 'true' + : null, + jenisPengangkatan: json['jenis_pengangkatan'], + detailPekerjaan: json['detail_pekerjaan'], + idTarif: json['id_tarif'] != null + ? int.tryParse(json['id_tarif'].toString()) + : null, + totalNilaiPekerjaan: json['total_nilai_pekerjaan'] != null + ? double.tryParse(json['total_nilai_pekerjaan'].toString()) + : null, + statusPekerjaan: json['status_pekerjaan'] ?? 'belum_mulai', + tanggalMulai: json['tanggal_mulai'], + tanggalDiselesaikan: json['tanggal_diselesaikan'], + tanggalGaransiMulai: json['tanggal_garansi_mulai'], + tanggalGaransiSelesai:json['tanggal_garansi_selesai'], + catatanGaransi: json['catatan_garansi'], + isGaransiAktif: json['is_garansi_aktif'] != null + ? json['is_garansi_aktif'] == 1 || + json['is_garansi_aktif'] == true || + json['is_garansi_aktif'].toString() == '1' || + json['is_garansi_aktif'].toString() == 'true' + : null, + sisaHariGaransi: json['sisa_hari_garansi'] != null + ? int.tryParse(json['sisa_hari_garansi'].toString()) + : null, + fotoSebelum: json['foto_sebelum'], + fotoSesudah: json['foto_sesudah'], + fotoSebelumUrl: json['foto_sebelum_url'], + fotoSesudahUrl: json['foto_sesudah_url'], + teknisi: json['teknisi'] != null + ? TeknisiModel.fromJson(json['teknisi']) + : null, + tarif: json['tarif'] != null + ? TarifModel.fromJson(json['tarif']) + : null, + timTeknisi: json['tim_teknisi'] != null + ? (json['tim_teknisi'] as List) + .map((e) => TimTeknisiModel.fromJson(e)) + .toList() + : null, + ); + } + + Map toJson() { + return { + 'id_penugasan': idPenugasan, + 'id_teknisi': idTeknisi, + 'foto_surat': fotoSurat, + 'tanggal_diberikan': tanggalDiberikan, + 'catatan_admin': catatanAdmin, + 'alamat_lokasi': alamatLokasi, + 'nama_pelanggan': namaPelanggan, + 'no_sambungan': noSambungan, + 'jenis_pekerjaan': jenisPekerjaan, + 'dimensi_pipa': dimensiPipa, + 'jarak_meter': jarakMeter, + 'jumlah_unit': jumlahUnit, + 'jumlah_titik': jumlahTitik, + 'pakai_pipa_besi': pakaiPipaBesi, + 'jenis_pengangkatan': jenisPengangkatan, + 'detail_pekerjaan': detailPekerjaan, + 'id_tarif': idTarif, + 'total_nilai_pekerjaan': totalNilaiPekerjaan, + 'status_pekerjaan': statusPekerjaan, + }; + } + + // =================================== + // HELPER + // =================================== + + bool get isDetailLengkap => jenisPekerjaan != null; + + String get statusLabel { + switch (statusPekerjaan) { + case 'belum_mulai': return 'Belum Mulai'; + case 'dalam_proses': return 'Dalam Proses'; + case 'selesai': return 'Selesai'; + case 'dibatalkan': return 'Dibatalkan'; + default: return statusPekerjaan; + } + } + + String get statusColor { + switch (statusPekerjaan) { + case 'belum_mulai': return '#9E9E9E'; + case 'dalam_proses': return '#FFA500'; + case 'selesai': return '#4CAF50'; + case 'dibatalkan': return '#F44336'; + default: return '#9E9E9E'; + } + } + + String get labelJenisPekerjaanFallback { + if (labelJenisPekerjaan != null) return labelJenisPekerjaan!; + switch (jenisPekerjaan) { + case 'sr': return 'SR (Sambungan Rumah)'; + case 'pengembangan_jaringan_pipa': return 'Pengembangan Jaringan Pipa'; + case 'pengangkatan': return 'Pengangkatan'; + case 'pemasangan_gate_valve': return 'Pemasangan Gate Valve'; + case 'gali_urug': return 'Gali Urug'; + case 'perbaikan_jaringan_pipa': return 'Perbaikan Jaringan Pipa'; + case 'pengecatan_pipa_besi': return 'Pengecatan Pipa Besi'; + case 'penyempurnaan_jaringan_pipa': return 'Penyempurnaan Jaringan Pipa'; + default: return jenisPekerjaan ?? '-'; + } + } + + String get totalNilaiFormatted { + if (totalNilaiPekerjaan == null) return '-'; + final formatted = totalNilaiPekerjaan!.toStringAsFixed(0) + .replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (m) => '${m[1]}.'); + return 'Rp $formatted'; + } + + // ✅ Getter untuk mengambil semua nama tim + String get namaTim { + if (timTeknisi == null || timTeknisi!.isEmpty) return teknisi?.nama ?? '-'; + // Gabungkan nama unik agar tidak double + final names = timTeknisi! + .map((e) => e.teknisi?.nama ?? '') + .where((n) => n.isNotEmpty) + .toSet() + .toList(); + return names.isNotEmpty ? names.join(', ') : (teknisi?.nama ?? '-'); + } +} + +// =================================== +// TEKNISI MODEL +// =================================== +class TeknisiModel { + final int idTeknisi; + final String nama; + final String? noTelepon; + final String? spesialisasi; + + TeknisiModel({ + required this.idTeknisi, + required this.nama, + this.noTelepon, + this.spesialisasi, + }); + + factory TeknisiModel.fromJson(Map json) { + return TeknisiModel( + idTeknisi: int.tryParse(json['id_teknisi']?.toString() ?? '') ?? 0, + nama: json['nama'] ?? '', + noTelepon: json['no_telepon'], + spesialisasi: json['spesialisasi'], + ); + } +} + +// =================================== +// TARIF MODEL +// =================================== +class TarifModel { + final int idTarif; + final String jenisPekerjaan; + final String namaItem; + final String kodeItem; + final String? dimensiPipa; + final double? tarifPerUnit; + final double? tarifPerMeter; + final bool? pakaiPipaBesi; + final bool adaGaransi; + final int? durasiGaransiBulan; + + TarifModel({ + required this.idTarif, + required this.jenisPekerjaan, + required this.namaItem, + required this.kodeItem, + this.dimensiPipa, + this.tarifPerUnit, + this.tarifPerMeter, + this.pakaiPipaBesi, + required this.adaGaransi, + this.durasiGaransiBulan, + }); + + factory TarifModel.fromJson(Map json) { + return TarifModel( + idTarif: int.tryParse(json['id_tarif']?.toString() ?? '') ?? 0, + jenisPekerjaan: json['jenis_pekerjaan'] ?? '', + namaItem: json['nama_item'] ?? '', + kodeItem: json['kode_item'] ?? '', + dimensiPipa: json['dimensi_pipa'], + tarifPerUnit: json['tarif_per_unit'] != null + ? double.tryParse(json['tarif_per_unit'].toString()) + : null, + tarifPerMeter: json['tarif_per_meter'] != null + ? double.tryParse(json['tarif_per_meter'].toString()) + : null, + pakaiPipaBesi: json['pakai_pipa_besi'] != null + ? json['pakai_pipa_besi'] == 1 || + json['pakai_pipa_besi'] == true || + json['pakai_pipa_besi'].toString() == '1' || + json['pakai_pipa_besi'].toString() == 'true' + : null, + adaGaransi: json['ada_garansi'] == 1 || + json['ada_garansi'] == true || + json['ada_garansi'].toString() == '1' || + json['ada_garansi'].toString() == 'true', + durasiGaransiBulan: json['durasi_garansi_bulan'] != null + ? int.tryParse(json['durasi_garansi_bulan'].toString()) + : null, + ); + } +} + +// =================================== +// TIM TEKNISI MODEL +// =================================== +class TimTeknisiModel { + final int idTimTeknisi; + final int idPenugasan; + final int idTeknisi; + final String statusKehadiran; + final TeknisiModel? teknisi; + + TimTeknisiModel({ + required this.idTimTeknisi, + required this.idPenugasan, + required this.idTeknisi, + required this.statusKehadiran, + this.teknisi, + }); + + factory TimTeknisiModel.fromJson(Map json) { + return TimTeknisiModel( + idTimTeknisi: int.tryParse(json['id_tim_penugasan']?.toString() ?? json['id_tim_teknisi']?.toString() ?? '') ?? 0, + idPenugasan: int.tryParse(json['id_penugasan']?.toString() ?? '') ?? 0, + idTeknisi: int.tryParse(json['id_teknisi']?.toString() ?? '') ?? 0, + statusKehadiran: json['status_kehadiran'] ?? 'hadir', + teknisi: json['teknisi'] != null + ? TeknisiModel.fromJson(json['teknisi']) + : null, + ); + } +} + +// =================================== +// STATISTIK MODEL +// =================================== +class StatistikModel { + final int totalPenugasan; + final int belumMulai; + final int dalamProses; + final int selesai; + final int menungguDetail; + final int detailLengkap; + + StatistikModel({ + required this.totalPenugasan, + required this.belumMulai, + required this.dalamProses, + required this.selesai, + required this.menungguDetail, + required this.detailLengkap, + }); + + factory StatistikModel.fromJson(Map json) { + return StatistikModel( + totalPenugasan: int.tryParse(json['total_penugasan']?.toString() ?? '') ?? 0, + belumMulai: int.tryParse(json['belum_mulai']?.toString() ?? '') ?? 0, + dalamProses: int.tryParse(json['dalam_proses']?.toString() ?? '') ?? 0, + selesai: int.tryParse(json['selesai']?.toString() ?? '') ?? 0, + menungguDetail: int.tryParse(json['menunggu_detail']?.toString() ?? '') ?? 0, + detailLengkap: int.tryParse(json['detail_lengkap']?.toString() ?? '') ?? 0, + ); + } +} \ No newline at end of file diff --git a/samooflutter/lib/profil/Profil.dart b/samooflutter/lib/profil/Profil.dart new file mode 100644 index 0000000..f6d5c73 --- /dev/null +++ b/samooflutter/lib/profil/Profil.dart @@ -0,0 +1,181 @@ +import 'package:flutter/material.dart'; +import '../api/LoginApi.dart'; +import 'package:intl/intl.dart'; + +const _bg = Color(0xFFF9FAFB); +const _bg1 = Color(0xFFFFFFFF); +const _bg2 = Color(0xFFF3F4F6); +const _green = Color(0xFF10B981); +const _greenDim = Color(0x1A10B981); +const _rose = Color(0xFFEF4444); +const _t1 = Color(0xFF111827); +const _t2 = Color(0xFF6B7280); +const _t3 = Color(0xFF9CA3AF); +const _line2 = Color(0xFFE5E7EB); + +class ProfilScreen extends StatefulWidget { + const ProfilScreen({super.key}); + + @override + State createState() => _ProfilScreenState(); +} + +class _ProfilScreenState extends State { + final ApiService _apiService = ApiService(); + Map? _userData; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _loadProfile(); + } + + Future _loadProfile() async { + setState(() => _isLoading = true); + final res = await _apiService.getProfile(); + if (mounted) { + setState(() { + _isLoading = false; + if (res['success'] == true) _userData = res['data']; + }); + } + } + + String _fmtDate(dynamic date) { + if (date == null) return '-'; + try { + final dt = DateTime.parse(date.toString()); + return DateFormat('dd MMM yyyy').format(dt); + } catch (e) { + return date.toString(); + } + } + + @override + Widget build(BuildContext context) { + final teknisi = _userData?['teknisi']; + final user = _userData; + final nama = teknisi?['nama'] ?? 'Teknisi'; + final initial = nama.isNotEmpty ? nama[0].toUpperCase() : 'T'; + + return Scaffold( + backgroundColor: _bg, + appBar: AppBar( + elevation: 0, + backgroundColor: _bg, + title: const Text('Profil Saya', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w800, color: _t1)), + centerTitle: true, + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator(color: _green)) + : SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), + child: Column( + children: [ + // Header Profile + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: _bg1, + borderRadius: BorderRadius.circular(32), + border: Border.all(color: _line2), + ), + child: Column( + children: [ + Container( + width: 80, height: 80, + decoration: BoxDecoration( + gradient: const LinearGradient(colors: [_green, Color(0xFF00c960)]), + borderRadius: BorderRadius.circular(24), + ), + child: Center(child: Text(initial, style: const TextStyle(fontSize: 32, fontWeight: FontWeight.w900, color: Colors.black))), + ), + const SizedBox(height: 16), + Text(nama, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w800, color: _t1)), + const SizedBox(height: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration(color: _greenDim, borderRadius: BorderRadius.circular(100)), + child: const Text('TEKNISI AKTIF', style: TextStyle(fontSize: 9, fontWeight: FontWeight.w900, color: _green, letterSpacing: 1)), + ), + ], + ), + ), + + const SizedBox(height: 24), + + // Data Lengkap Teknisi (Sesuai Admin) + _buildInfoGroup('DATA PEGAWAI', [ + _infoTile(Icons.badge_outlined, 'ID TEKNISI', 'ID: ${teknisi?['id_teknisi'] ?? '-'}'), + _infoTile(Icons.email_outlined, 'KONTAK EMAIL', user?['email'] ?? teknisi?['email'] ?? '-'), + _infoTile(Icons.phone_iphone_rounded, 'NO. TELEPON', teknisi?['no_telephone'] ?? '-'), + _infoTile(Icons.location_on_outlined, 'ALAMAT', teknisi?['alamat'] ?? '-'), + ]), + + const SizedBox(height: 32), + + // Logout + SizedBox( + width: double.infinity, + child: TextButton.icon( + onPressed: () => _handleLogout(context), + icon: const Icon(Icons.logout_rounded, size: 18), + label: const Text('Keluar dari Akun', style: TextStyle(fontWeight: FontWeight.w800)), + style: TextButton.styleFrom( + foregroundColor: _rose, + padding: const EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + const SizedBox(height: 20), + ], + ), + ), + ); + } + + Widget _buildInfoGroup(String title, List children) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 4, bottom: 8), + child: Text(title, style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w800, color: _t3, letterSpacing: 1.2)), + ), + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration(color: _bg1, borderRadius: BorderRadius.circular(24), border: Border.all(color: _line2)), + child: Column(children: children), + ), + ], + ); + } + + Widget _infoTile(IconData icon, String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + children: [ + Icon(icon, color: _green.withOpacity(0.7), size: 18), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: const TextStyle(color: _t3, fontSize: 9, fontWeight: FontWeight.w800)), + const SizedBox(height: 2), + Text(value, style: const TextStyle(color: _t1, fontSize: 13, fontWeight: FontWeight.w600)), + ], + ), + ) + ], + ), + ); + } + + Future _handleLogout(BuildContext context) async { + Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false); + } +} diff --git a/samooflutter/lib/tugas/Penugasan.dart b/samooflutter/lib/tugas/Penugasan.dart new file mode 100644 index 0000000..f78735a --- /dev/null +++ b/samooflutter/lib/tugas/Penugasan.dart @@ -0,0 +1,953 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:intl/intl.dart'; +import 'package:image_picker/image_picker.dart'; +import '../models/Penugasan_model.dart'; +import '../api/PenugasanApi.dart'; + +const _bg = Color(0xFFF9FAFB); +const _bg1 = Color(0xFFFFFFFF); +const _bg2 = Color(0xFFF3F4F6); +const _green = Color(0xFF10B981); +const _greenDim = Color(0x1A10B981); +const _cyan = Color(0xFF06B6D4); +const _cyanDim = Color(0x1A06B6D4); +const _amber = Color(0xFFF59E0B); +const _amberDim = Color(0x1AF59E0B); +const _rose = Color(0xFFEF4444); +const _roseDim = Color(0x1AEF4444); +const _t1 = Color(0xFF111827); +const _t2 = Color(0xFF6B7280); +const _t3 = Color(0xFF9CA3AF); +const _line2 = Color(0xFFE5E7EB); + +class PenugasanScreen extends StatefulWidget { + @override + _PenugasanScreenState createState() => _PenugasanScreenState(); +} + +class _PenugasanScreenState extends State { + final _api = PenugasanApi(); + + List filteredList = []; + StatistikModel? statistik; + bool isLoading = false; + String? errorMessage; + + @override + void initState() { + super.initState(); + _loadData(); + } + + Future _loadData() async { + setState(() { isLoading = true; errorMessage = null; }); + try { + final results = await Future.wait([_api.getStatistik(), _api.getPenugasanList()]); + final stat = StatistikModel.fromJson(results[0]['data']); + final items = results[1]['data']['data'] as List? ?? []; + final list = items.map((e) => PenugasanModel.fromJson(e)).toList(); + + // HANYA tampilkan tugas belum mulai + final filtered = list.where((p) => p.statusPekerjaan == 'belum_mulai').toList(); + + if (mounted) setState(() { statistik = stat; filteredList = filtered; isLoading = false; }); + } on PenugasanApiException catch (e) { + if (mounted) setState(() { errorMessage = e.message; isLoading = false; }); + } catch (_) { + if (mounted) setState(() { errorMessage = 'Gagal terhubung ke server.'; isLoading = false; }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: _bg, + appBar: AppBar( + title: Text('Penugasan Saya', style: TextStyle(fontWeight: FontWeight.w800, fontSize: 16)), + backgroundColor: _bg1, + foregroundColor: _t1, + elevation: 0, + surfaceTintColor: Colors.transparent, + systemOverlayStyle: SystemUiOverlayStyle(statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.light), + bottom: PreferredSize( + preferredSize: Size.fromHeight(60), + child: Container( + padding: EdgeInsets.fromLTRB(16, 0, 16, 16), + alignment: Alignment.centerLeft, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('DAFTAR TUGAS BARU', style: TextStyle(color: _cyan, fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 1.5)), + SizedBox(height: 4), + Text('Terima tugas dan mulai bekerja', style: TextStyle(color: _t2, fontSize: 12)), + ]), + ), + ), + ), + body: _buildBody(), + ); + } + + Widget _buildStatistikCard() { + if (statistik == null) return SizedBox.shrink(); + return Container( + padding: EdgeInsets.all(16), + color: _bg, + child: Row(children: [ + Expanded(child: _buildStatItem('Menunggu Detail', statistik!.menungguDetail.toString(), _amber, _amberDim, Icons.pending_actions)), + SizedBox(width: 12), + Expanded(child: _buildStatItem('Detail Lengkap', statistik!.detailLengkap.toString(), _green, _greenDim, Icons.check_circle_outline)), + ]), + ); + } + + Widget _buildStatItem(String label, String value, Color color, Color dimColor, IconData icon) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration(color: _bg1, borderRadius: BorderRadius.circular(13), border: Border.all(color: _line2)), + child: Row(children: [ + Container( + width: 38, height: 38, + decoration: BoxDecoration(color: dimColor, borderRadius: BorderRadius.circular(10), border: Border.all(color: color.withOpacity(0.3))), + child: Icon(icon, color: color, size: 20), + ), + SizedBox(width: 10), + Expanded( + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(value, style: TextStyle(fontSize: 18, fontWeight: FontWeight.w800, color: color)), + SizedBox(height: 2), + Text(label, style: TextStyle(fontSize: 11, color: _t2), maxLines: 1, overflow: TextOverflow.ellipsis), + ]), + ), + ]), + ); + } + + Widget _buildBody() { + if (isLoading) return Center(child: CircularProgressIndicator(color: _green)); + if (errorMessage != null) return Center(child: Padding( + padding: EdgeInsets.all(24), + child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(Icons.wifi_off, size: 60, color: _rose), + SizedBox(height: 16), + Text(errorMessage!, textAlign: TextAlign.center, style: TextStyle(color: _t1)), + SizedBox(height: 20), + ElevatedButton.icon(onPressed: _loadData, icon: Icon(Icons.refresh, size: 18), label: Text('Coba Lagi'), + style: ElevatedButton.styleFrom(backgroundColor: _greenDim, foregroundColor: _green, side: BorderSide(color: _green.withOpacity(0.4)))), + ]), + )); + if (filteredList.isEmpty) return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(Icons.assignment_outlined, size: 60, color: _t3), + SizedBox(height: 12), + Text('Belum ada penugasan', style: TextStyle(fontSize: 15, color: _t2, fontWeight: FontWeight.w500)), + ])); + + return RefreshIndicator( + onRefresh: _loadData, + color: _green, + backgroundColor: _bg1, + child: ListView.builder( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + itemCount: filteredList.length, + itemBuilder: (context, index) => _buildCard(filteredList[index]), + ), + ); + } + + Widget _buildCard(PenugasanModel item) { + return Container( + margin: EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: _bg1, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: _line2), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _showDetail(item), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Foto & Instruksi Utama + if (item.fotoSuratUrl != null) + Image.network(item.fotoSuratUrl!, height: 160, width: double.infinity, fit: BoxFit.cover, + loadingBuilder: (c, child, p) => p == null ? child : Container(height: 160, color: _bg2, child: Center(child: CircularProgressIndicator(color: _green, strokeWidth: 2))), + errorBuilder: (_, __, ___) => Container(height: 100, color: _bg2, child: Center(child: Icon(Icons.image_not_supported_outlined, color: _t3)))) + else + Container( + width: double.infinity, + padding: EdgeInsets.fromLTRB(16, 20, 16, 12), + decoration: BoxDecoration( + gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [_bg2, _bg1]), + border: Border(bottom: BorderSide(color: _line2)) + ), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('INSTRUKSI LOKASI & TUGAS', style: TextStyle(color: _cyan, fontSize: 10, fontWeight: FontWeight.w900, letterSpacing: 1.5)), + SizedBox(height: 10), + Text(item.catatanAdmin ?? 'Tidak ada catatan lokasi spesifik', + style: TextStyle(color: _t1, fontSize: 14, fontWeight: FontWeight.w600, height: 1.4), + maxLines: 3, overflow: TextOverflow.ellipsis), + ]), + ), + + Padding( + padding: EdgeInsets.all(16), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Row(children: [ + Icon(Icons.calendar_today_outlined, size: 14, color: _t3), + SizedBox(width: 6), + Text(DateFormat('dd MMM yyyy').format(DateTime.parse(item.tanggalDiberikan).toLocal()), style: TextStyle(fontSize: 13, color: _t2)), + ]), + _buildStatusBadge(item.statusPekerjaan), + ]), + SizedBox(height: 12), + if (item.teknisi != null) Row(children: [ + Icon(Icons.person, size: 16, color: _cyan), + SizedBox(width: 8), + Expanded(child: Text(item.namaTim, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: _t1))), + ]), + SizedBox(height: 12), + if (item.isDetailLengkap) ...[ + Container( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration(color: _cyanDim, borderRadius: BorderRadius.circular(8), border: Border.all(color: _cyan.withOpacity(0.3))), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Icon(Icons.build_circle_outlined, size: 14, color: _cyan), + SizedBox(width: 6), + Text(item.labelJenisPekerjaanFallback, style: TextStyle(fontSize: 12, color: _cyan, fontWeight: FontWeight.w600)), + ]), + ), + ], + if (item.alamatLokasi != null) ...[ + SizedBox(height: 10), + Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration(color: _bg2, borderRadius: BorderRadius.circular(10), border: Border.all(color: _line2)), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(children: [ + Icon(Icons.location_on, size: 14, color: _green), + SizedBox(width: 8), + Expanded(child: Text(item.alamatLokasi!, style: TextStyle(fontSize: 13, color: _t1, fontWeight: FontWeight.w600))), + ]), + if (item.namaPelanggan != null || item.noSambungan != null) ...[ + SizedBox(height: 8), + Row(children: [ + Icon(Icons.person_outline, size: 14, color: _t2), + SizedBox(width: 8), + Text(item.namaPelanggan ?? '-', style: TextStyle(fontSize: 12, color: _t2)), + SizedBox(width: 12), + Icon(Icons.tag, size: 14, color: _t2), + SizedBox(width: 4), + Text(item.noSambungan ?? '-', style: TextStyle(fontSize: 12, color: _t2)), + ]), + ], + ]), + ), + ], + if (item.catatanAdmin != null) ...[ + SizedBox(height: 10), + Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration(color: _amberDim, borderRadius: BorderRadius.circular(10), border: Border.all(color: _amber.withOpacity(0.3))), + child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Icon(Icons.info_outline, size: 16, color: _amber), + SizedBox(width: 8), + Expanded(child: Text(item.catatanAdmin!, style: TextStyle(fontSize: 12, color: _amber, height: 1.4), maxLines: 2, overflow: TextOverflow.ellipsis)), + ]), + ), + ], + SizedBox(height: 14), + Divider(height: 1, color: _line2), + SizedBox(height: 14), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () => _terimaTugas(item), + icon: Icon(Icons.check_rounded, size: 18), + label: Text('TERIMA TUGAS', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w800, letterSpacing: 0.8)), + style: ElevatedButton.styleFrom( + backgroundColor: _green, + foregroundColor: Colors.black, + elevation: 0, + padding: EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + ), + ), + ]), + ), + ]), + ), + ), + ), + ); + } + + Widget _labelChip(String label, Color color) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration(color: color.withOpacity(0.08), borderRadius: BorderRadius.circular(12), border: Border.all(color: color.withOpacity(0.3))), + child: Text(label, style: TextStyle(fontSize: 10, color: color, fontWeight: FontWeight.w700, letterSpacing: 0.5)), + ); + } + + Widget _actionButton(String label, IconData icon, Color color, VoidCallback onTap) { + return ElevatedButton.icon( + onPressed: onTap, + icon: Icon(icon, size: 16), + label: Text(label, style: TextStyle(fontWeight: FontWeight.w700, fontSize: 13)), + style: ElevatedButton.styleFrom( + backgroundColor: color.withOpacity(0.15), + foregroundColor: color, + shadowColor: Colors.transparent, + side: BorderSide(color: color.withOpacity(0.5)), + padding: EdgeInsets.symmetric(horizontal: 14, vertical: 8), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))), + ); + } + + Widget _buildStatusBadge(String status) { + Color bg, text; String label; IconData icon; + switch (status) { + case 'belum_mulai': bg = _bg2; text = _t2; label = 'Belum Mulai'; icon = Icons.schedule; break; + case 'dalam_proses': bg = _amberDim; text = _amber; label = 'Dalam Proses'; icon = Icons.pending_actions; break; + case 'selesai': bg = _greenDim; text = _green; label = 'Selesai'; icon = Icons.check_circle_outline; break; + default: bg = _bg2; text = _t3; label = status; icon = Icons.help_outline; + } + return Container( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(12), border: Border.all(color: text.withOpacity(0.3))), + child: Row(mainAxisSize: MainAxisSize.min, children: [Icon(icon, size: 12, color: text), SizedBox(width: 5), Text(label, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: text, letterSpacing: 0.5))]), + ); + } + + void _toast(String msg, {bool isError = false}) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(msg, style: TextStyle(color: _t1)), + backgroundColor: _bg1, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10), side: BorderSide(color: (isError ? _rose : _green).withOpacity(0.5))), + behavior: SnackBarBehavior.floating, + )); + } + + Future _terimaTugas(PenugasanModel item) async { + setState(() => isLoading = true); + try { + await _api.updateStatus( + idPenugasan: item.idPenugasan, + statusPekerjaan: 'dalam_proses', + tanggalDiselesaikan: null, + ); + _loadData(); + _toast('Tugas berhasil diterima! Cek menu Progress.'); + } catch (e) { + setState(() => isLoading = false); + _toast('Gagal: $e', isError: true); + } + } + + void _showFormIsiDetail(PenugasanModel item) { + showModalBottomSheet( + context: context, isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => _FormIsiDetail(item: item, api: _api, onSuccess: () { + Navigator.pop(context); + _loadData(); + _toast('Detail berhasil diisi!'); + }, onFail: (err) => _toast(err, isError: true)), + ); + } + + void _showFormUpdateProgres(PenugasanModel item) { + showModalBottomSheet( + context: context, isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => _FormUpdateProgres(item: item, api: _api, onSuccess: () { + Navigator.pop(context); + _loadData(); + _toast('Progres berhasil diupdate!'); + }, onFail: (err) => _toast(err, isError: true)), + ); + } + + void _showDetail(PenugasanModel item) { + showModalBottomSheet( + context: context, isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => DraggableScrollableSheet( + expand: false, initialChildSize: 0.75, maxChildSize: 0.95, + builder: (_, controller) => Container( + decoration: BoxDecoration( + color: _bg1, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + border: Border(top: BorderSide(color: _line2)) + ), + child: SingleChildScrollView( + controller: controller, padding: EdgeInsets.all(24), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Center(child: Container(width: 48, height: 5, decoration: BoxDecoration(color: _bg2, borderRadius: BorderRadius.circular(3)))), + SizedBox(height: 24), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text('Detail Tugas #${item.idPenugasan}', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w800, color: _t1)), + _buildStatusBadge(item.statusPekerjaan), + ]), + SizedBox(height: 24), + if (item.fotoSuratUrl != null) ...[ + Text('FOTO SURAT TUGAS', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w800, color: _t3, letterSpacing: 1.2)), + SizedBox(height: 10), + ClipRRect(borderRadius: BorderRadius.circular(12), + child: Image.network(item.fotoSuratUrl!, width: double.infinity, fit: BoxFit.cover, + loadingBuilder: (c, child, p) => p == null ? child : Container(height: 180, color: _bg2, child: Center(child: CircularProgressIndicator(color: _green))), + errorBuilder: (_, __, ___) => Container(height: 100, color: _bg2, child: Center(child: Icon(Icons.image_not_supported_outlined, color: _t3))))), + SizedBox(height: 24), + ], + + _detailRow('Teknisi', item.namaTim), + _detailRow('Tanggal Tugas', DateFormat('dd MMMM yyyy').format(DateTime.parse(item.tanggalDiberikan).toLocal())), + if (item.alamatLokasi != null) _detailRow('Alamat Lokasi', item.alamatLokasi!, isHighlight: true, valColor: _green), + if (item.namaPelanggan != null) _detailRow('Nama Pelanggan', item.namaPelanggan!), + if (item.noSambungan != null) _detailRow('No. Sambungan', item.noSambungan!), + if (item.catatanAdmin != null) _detailRow('Instruksi Tambahan', item.catatanAdmin!), + + if (item.isDetailLengkap) ...[ + SizedBox(height: 10), Divider(height: 1, color: _line2), SizedBox(height: 16), + Text('DETAIL PEKERJAAN', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w800, color: _cyan, letterSpacing: 1.2)), + SizedBox(height: 12), + _detailRow('Jenis Pekerjaan', item.labelJenisPekerjaanFallback), + if (item.dimensiPipa != null) _detailRow('Dimensi Pipa', 'Dim ${item.dimensiPipa}'), + if (item.jarakMeter != null) _detailRow('Jarak', '${item.jarakMeter} meter'), + if (item.jumlahUnit != null) _detailRow('Jumlah Unit', '${item.jumlahUnit} unit'), + if (item.jumlahTitik != null) _detailRow('Jumlah Titik', '${item.jumlahTitik} titik'), + if (item.pakaiPipaBesi != null) _detailRow('Pakai Pipa Besi', item.pakaiPipaBesi! ? 'Ya' : 'Tidak'), + if (item.jenisPengangkatan != null) _detailRow('Jenis Pengangkatan', item.jenisPengangkatan == 'gate_valve' ? 'Gate Valve' : 'Meteran Air'), + if (item.detailPekerjaan != null) _detailRow('Catatan Teknisi', item.detailPekerjaan!), + if (item.tanggalMulai != null) _detailRow('Tanggal Mulai', item.tanggalMulai!), + if (item.tanggalDiselesaikan != null) _detailRow('Waktu Selesai', item.tanggalDiselesaikan!), + ], + + if (item.fotoSebelumUrl != null || item.fotoSesudahUrl != null) ...[ + SizedBox(height: 10), Divider(height: 1, color: _line2), SizedBox(height: 16), + Text('FOTO BUKTI', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w800, color: _t3, letterSpacing: 1.2)), + SizedBox(height: 12), + Row(children: [ + if (item.fotoSebelumUrl != null) Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Sebelum', style: TextStyle(fontSize: 12, color: _t2)), + SizedBox(height: 8), + ClipRRect(borderRadius: BorderRadius.circular(10), child: Image.network(item.fotoSebelumUrl!, height: 100, fit: BoxFit.cover, width: double.infinity, + errorBuilder: (_, __, ___) => Container(height: 100, color: _bg2, child: Center(child: Icon(Icons.image_not_supported_outlined, color: _t3))))), + ])), + if (item.fotoSebelumUrl != null && item.fotoSesudahUrl != null) SizedBox(width: 16), + if (item.fotoSesudahUrl != null) Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Sesudah', style: TextStyle(fontSize: 12, color: _t2)), + SizedBox(height: 8), + ClipRRect(borderRadius: BorderRadius.circular(10), child: Image.network(item.fotoSesudahUrl!, height: 100, fit: BoxFit.cover, width: double.infinity, + errorBuilder: (_, __, ___) => Container(height: 100, color: _bg2, child: Center(child: Icon(Icons.image_not_supported_outlined, color: _t3))))), + ])), + ]), + ], + SizedBox(height: 32), + + if (item.statusPekerjaan == 'belum_mulai') + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () { + Navigator.pop(context); + _terimaTugas(item); + }, + icon: Icon(Icons.check_rounded, size: 18), + label: Text('TERIMA TUGAS', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800, letterSpacing: 0.8)), + style: ElevatedButton.styleFrom( + backgroundColor: _green, + foregroundColor: Colors.black, + elevation: 0, + padding: EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + ), + ), + SizedBox(height: 20), + ]), + ), + ), + ), + ); + } + + Widget _detailRow(String label, String value, {bool isHighlight = false, Color valColor = _t1}) { + return Padding( + padding: EdgeInsets.only(bottom: 12), + child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox(width: 140, child: Text(label, style: TextStyle(fontSize: 13, color: _t2))), + Expanded(child: Container( + padding: isHighlight ? EdgeInsets.all(10) : null, + decoration: isHighlight ? BoxDecoration(color: _bg2, borderRadius: BorderRadius.circular(8), border: Border.all(color: _line2)) : null, + child: Text(value, style: TextStyle(fontSize: isHighlight ? 13 : 13.5, fontWeight: isHighlight ? FontWeight.w500 : FontWeight.w600, color: valColor, height: 1.4)) + )), + ]), + ); + } + + ButtonStyle _btnStyle(Color c) => ElevatedButton.styleFrom( + backgroundColor: c, foregroundColor: Colors.black, elevation: 0, + padding: EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ); +} + +// =================================== +// FORM ISI DETAIL +// =================================== +class _FormIsiDetail extends StatefulWidget { + final PenugasanModel item; + final PenugasanApi api; + final VoidCallback onSuccess; + final Function(String) onFail; + const _FormIsiDetail({required this.item, required this.api, required this.onSuccess, required this.onFail}); + @override + __FormIsiDetailState createState() => __FormIsiDetailState(); +} + +class __FormIsiDetailState extends State<_FormIsiDetail> { + final _formKey = GlobalKey(); + bool _isLoading = false; + String? _tanggalMulai; + final _detailController = TextEditingController(); + XFile? _fotoSebelum; + final _picker = ImagePicker(); + + // List item pekerjaan + List> _items = [ + { + 'jenis_pekerjaan': null, + 'dimensi_pipa': null, + 'jarak_meter': TextEditingController(), + 'jumlah_unit': TextEditingController(), + 'jumlah_titik': TextEditingController(), + 'pakai_pipa_besi': false, + 'jenis_pengangkatan': null, + } + ]; + + final List> _jenisList = [ + {'value': 'sr', 'label': 'SR (Sambungan Rumah)'}, + {'value': 'pengembangan_jaringan_pipa', 'label': 'Pengembangan Jaringan Pipa'}, + {'value': 'pengangkatan', 'label': 'Pengangkatan'}, + {'value': 'pemasangan_gate_valve', 'label': 'Pemasangan Gate Valve'}, + {'value': 'gali_urug', 'label': 'Gali Urug'}, + {'value': 'perbaikan_jaringan_pipa', 'label': 'Perbaikan Jaringan Pipa'}, + {'value': 'pengecatan_pipa_besi', 'label': 'Pengecatan Pipa Besi'}, + {'value': 'penyempurnaan_jaringan_pipa', 'label': 'Penyempurnaan Jaringan Pipa'}, + ]; + + @override + void dispose() { + _detailController.dispose(); + for (var it in _items) { + (it['jarak_meter'] as TextEditingController).dispose(); + (it['jumlah_unit'] as TextEditingController).dispose(); + (it['jumlah_titik'] as TextEditingController).dispose(); + } + super.dispose(); + } + + void _addItem() { + setState(() { + _items.add({ + 'jenis_pekerjaan': null, + 'dimensi_pipa': null, + 'jarak_meter': TextEditingController(), + 'jumlah_unit': TextEditingController(), + 'jumlah_titik': TextEditingController(), + 'pakai_pipa_besi': false, + 'jenis_pengangkatan': null, + }); + }); + } + + void _removeItem(int index) { + if (_items.length > 1) { + setState(() { + ( _items[index]['jarak_meter'] as TextEditingController).dispose(); + ( _items[index]['jumlah_unit'] as TextEditingController).dispose(); + ( _items[index]['jumlah_titik'] as TextEditingController).dispose(); + _items.removeAt(index); + }); + } + } + + Future _pickFoto() async { + final picked = await _picker.pickImage(source: ImageSource.camera, imageQuality: 70); + if (picked != null) setState(() => _fotoSebelum = picked); + } + + Future _pickTanggal() async { + final now = DateTime.now(); + final picked = await showDatePicker(context: context, initialDate: now, firstDate: DateTime(now.year - 1), lastDate: now, + builder: (ctx, child) => Theme(data: ThemeData.light().copyWith( + colorScheme: ColorScheme.light(primary: _green, onPrimary: Colors.black, surface: _bg1, onSurface: _t1), + dialogBackgroundColor: _bg1, + ), child: child!)); + if (picked != null) setState(() => _tanggalMulai = DateFormat('yyyy-MM-dd').format(picked)); + } + + Future _submit() async { + if (!_formKey.currentState!.validate()) return; + if (_tanggalMulai == null) { widget.onFail('Pilih tanggal mulai'); return; } + if (_fotoSebelum == null && !kIsWeb) { widget.onFail('Foto sebelum pekerjaan wajib diisi'); return; } + + setState(() => _isLoading = true); + try { + // Persiapkan list items untuk dikirim + final List> itemsToSubmit = _items.map((it) { + return { + 'jenis_pekerjaan': it['jenis_pekerjaan'], + 'dimensi_pipa': it['dimensi_pipa'], + 'jarak_meter': (it['jarak_meter'] as TextEditingController).text.isNotEmpty ? double.tryParse((it['jarak_meter'] as TextEditingController).text) : null, + 'jumlah_unit': (it['jumlah_unit'] as TextEditingController).text.isNotEmpty ? int.tryParse((it['jumlah_unit'] as TextEditingController).text) : null, + 'jumlah_titik': (it['jumlah_titik'] as TextEditingController).text.isNotEmpty ? int.tryParse((it['jumlah_titik'] as TextEditingController).text) : null, + 'pakai_pipa_besi': it['pakai_pipa_besi'], + 'jenis_pengangkatan': it['jenis_pengangkatan'], + }; + }).toList(); + + await widget.api.lengkapiDetail( + idPenugasan: widget.item.idPenugasan, + items: itemsToSubmit, + tanggalMulai: _tanggalMulai!, + detailPekerjaan: _detailController.text.isNotEmpty ? _detailController.text : null, + fotoSebelum: _fotoSebelum, + ); + widget.onSuccess(); + } on PenugasanApiException catch (e) { + setState(() => _isLoading = false); widget.onFail(e.getFirstError() ?? e.message); + } catch (e) { + setState(() => _isLoading = false); widget.onFail('Gagal: $e'); + } + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: DraggableScrollableSheet( + expand: false, initialChildSize: 0.85, maxChildSize: 0.95, + builder: (_, controller) => Container( + decoration: BoxDecoration(color: _bg1, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), border: Border(top: BorderSide(color: _line2))), + child: SingleChildScrollView( + controller: controller, padding: EdgeInsets.all(24), + child: Form(key: _formKey, child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Center(child: Container(width: 48, height: 5, decoration: BoxDecoration(color: _bg2, borderRadius: BorderRadius.circular(3)))), + SizedBox(height: 24), + Text('Isi Detail Pekerjaan', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w800, color: _t1)), + Text('Penugasan #${widget.item.idPenugasan}', style: TextStyle(fontSize: 13, color: _t3)), + SizedBox(height: 24), + + _label('Tanggal Mulai *'), + GestureDetector( + onTap: _pickTanggal, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 14, vertical: 14), + decoration: BoxDecoration(color: _bg2, border: Border.all(color: _line2), borderRadius: BorderRadius.circular(10)), + child: Row(children: [ + Icon(Icons.calendar_today_rounded, size: 18, color: _t2), + SizedBox(width: 10), + Text(_tanggalMulai ?? 'Pilih tanggal mulai', style: TextStyle(fontSize: 14, color: _tanggalMulai != null ? _t1 : _t3)), + ]), + ), + ), + SizedBox(height: 24), + + // LOOP ITEMS + ListView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: _items.length, + itemBuilder: (ctx, index) => _buildItemForm(index), + ), + + Center( + child: TextButton.icon( + onPressed: _addItem, + icon: Icon(Icons.add_circle_outline, color: _cyan), + label: Text('Tambah Jenis Pekerjaan Lain', style: TextStyle(color: _cyan, fontWeight: FontWeight.w700)), + ), + ), + SizedBox(height: 24), + + _label('Catatan Teknisi (Opsional)'), + TextFormField(controller: _detailController, maxLines: 2, style: TextStyle(color: _t1, fontSize: 14), decoration: _inputDecor(hint: 'Catatan tambahan terkait lapangan...')), + SizedBox(height: 16), + + _label('Foto Sebelum Pekerjaan *'), + GestureDetector( + onTap: _pickFoto, + child: Container( + height: 120, + decoration: BoxDecoration(color: _bg2, borderRadius: BorderRadius.circular(10), border: Border.all(color: _line2)), + child: _fotoSebelum != null + ? kIsWeb + ? Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [Icon(Icons.check_circle, color: _green, size: 36), SizedBox(height: 8), Text('Foto dipilih ✓', style: TextStyle(color: _green, fontWeight: FontWeight.w600))])) + : ClipRRect(borderRadius: BorderRadius.circular(10), child: Image.file(File(_fotoSebelum!.path), fit: BoxFit.cover, width: double.infinity)) + : Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Container(padding: EdgeInsets.all(10), decoration: BoxDecoration(color: _cyanDim, shape: BoxShape.circle), child: Icon(Icons.camera_alt_outlined, color: _cyan, size: 24)), + SizedBox(height: 10), + Text('Ambil Foto Sebelum', style: TextStyle(color: _t2, fontSize: 13, fontWeight: FontWeight.w500)) + ])), + ), + ), + SizedBox(height: 32), + + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _isLoading ? null : _submit, + style: ElevatedButton.styleFrom(backgroundColor: _green, foregroundColor: Colors.black, padding: EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))), + child: _isLoading ? SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.black, strokeWidth: 2)) : Text('KIRIM & SIMPAN', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800, letterSpacing: 1.2)), + ), + ), + SizedBox(height: 20), + ])), + ), + ), + ), + ); + } + + Widget _buildItemForm(int index) { + final item = _items[index]; + final jenisPek = item['jenis_pekerjaan']; + + return Container( + margin: EdgeInsets.only(bottom: 24), + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: _bg, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: _line2), + ), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text('PEKERJAAN #${index + 1}', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w800, color: _cyan, letterSpacing: 1)), + if (_items.length > 1) + IconButton( + onPressed: () => _removeItem(index), + icon: Icon(Icons.delete_outline, color: _rose, size: 20), + visualDensity: VisualDensity.compact, + ), + ]), + SizedBox(height: 12), + + _label('Jenis Pekerjaan *'), + DropdownButtonFormField( + value: jenisPek, hint: Text('Pilih jenis pekerjaan', style: TextStyle(color: _t3)), + dropdownColor: _bg2, style: TextStyle(color: _t1, fontSize: 13), + decoration: _inputDecor(), iconEnabledColor: _t2, + items: _jenisList.map((j) => DropdownMenuItem(value: j['value'], child: Text(j['label']!))).toList(), + onChanged: (val) { setState(() { _items[index]['jenis_pekerjaan'] = val; }); }, + validator: (v) => v == null ? 'Wajib' : null, + ), + SizedBox(height: 16), + + if (jenisPek != null) ...[ + if (['sr','pengembangan_jaringan_pipa','pemasangan_gate_valve','perbaikan_jaringan_pipa','pengecatan_pipa_besi','penyempurnaan_jaringan_pipa'].contains(jenisPek)) ...[ + _label('Dimensi Pipa'), + DropdownButtonFormField( + value: item['dimensi_pipa'], hint: Text('Pilih dimensi', style: TextStyle(color: _t3)), dropdownColor: _bg2, style: TextStyle(color: _t1, fontSize: 13), decoration: _inputDecor(), iconEnabledColor: _t2, + items: ['1-2','3','4','6','8','10','12'].map((d) => DropdownMenuItem(value: d, child: Text('Dim $d inch'))).toList(), + onChanged: (val) => setState(() => _items[index]['dimensi_pipa'] = val), + ), + SizedBox(height: 16), + ], + + if (['pengembangan_jaringan_pipa','penyempurnaan_jaringan_pipa'].contains(jenisPek)) ...[ + _label('Jarak (meter)'), + TextFormField(controller: item['jarak_meter'], keyboardType: TextInputType.numberWithOptions(decimal: true), style: TextStyle(color: _t1, fontSize: 13), decoration: _inputDecor(hint: 'Contoh: 50.5', suffix: 'm')), + SizedBox(height: 16), + ], + + if (['sr','pengangkatan'].contains(jenisPek)) ...[ + _label('Jumlah Unit'), + TextFormField(controller: item['jumlah_unit'], keyboardType: TextInputType.number, style: TextStyle(color: _t1, fontSize: 13), decoration: _inputDecor(hint: 'Contoh: 2', suffix: 'unit')), + SizedBox(height: 16), + ], + + if (jenisPek == 'gali_urug') ...[ + _label('Jumlah Titik'), + TextFormField(controller: item['jumlah_titik'], keyboardType: TextInputType.number, style: TextStyle(color: _t1, fontSize: 13), decoration: _inputDecor(hint: 'Contoh: 3', suffix: 'titik')), + SizedBox(height: 16), + ], + + if (jenisPek == 'pemasangan_gate_valve') ...[ + Theme( + data: ThemeData(unselectedWidgetColor: _t3), + child: CheckboxListTile( + value: item['pakai_pipa_besi'], onChanged: (v) => setState(() => _items[index]['pakai_pipa_besi'] = v ?? false), + title: Text('Pakai Pipa Besi (+Rp 200rb)', style: TextStyle(fontSize: 12, color: _t1)), + activeColor: _green, checkColor: Colors.black, contentPadding: EdgeInsets.zero, + ), + ), + ], + + if (jenisPek == 'pengangkatan') ...[ + _label('Jenis Pengangkatan'), + DropdownButtonFormField( + value: item['jenis_pengangkatan'], hint: Text('Pilih jenis', style: TextStyle(color: _t3)), dropdownColor: _bg2, style: TextStyle(color: _t1, fontSize: 13), decoration: _inputDecor(), iconEnabledColor: _t2, + items: [DropdownMenuItem(value: 'meteran', child: Text('Meteran Air')), DropdownMenuItem(value: 'gate_valve', child: Text('Gate Valve'))], + onChanged: (val) => setState(() => _items[index]['jenis_pengangkatan'] = val), + ), + SizedBox(height: 16), + ], + ], + ]), + ); + } + + Widget _label(String text) => Padding(padding: EdgeInsets.only(bottom: 8), child: Text(text, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w800, color: _t2, letterSpacing: 0.8))); + + InputDecoration _inputDecor({String? hint, String? suffix}) => InputDecoration( + hintText: hint, hintStyle: TextStyle(color: _t3), suffixText: suffix, suffixStyle: TextStyle(color: _t2), + filled: true, fillColor: _bg2, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none), + enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: _line2)), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: _green, width: 1.5)), + contentPadding: EdgeInsets.symmetric(horizontal: 14, vertical: 14), + ); +} + +// =================================== +// FORM UPDATE PROGRES +// =================================== +class _FormUpdateProgres extends StatefulWidget { + final PenugasanModel item; + final PenugasanApi api; + final VoidCallback onSuccess; + final Function(String) onFail; + const _FormUpdateProgres({required this.item, required this.api, required this.onSuccess, required this.onFail}); + @override + __FormUpdateProgresState createState() => __FormUpdateProgresState(); +} + +class __FormUpdateProgresState extends State<_FormUpdateProgres> { + bool _isLoading = false; + bool _tandaiSelesai = false; + XFile? _fotoSesudah; + final _picker = ImagePicker(); + + Future _pickFoto() async { + final picked = await _picker.pickImage(source: ImageSource.camera, imageQuality: 70); + if (picked != null) setState(() => _fotoSesudah = picked); + } + + Future _submit() async { + setState(() => _isLoading = true); + try { + if (_fotoSesudah != null) { + await widget.api.uploadFoto(idPenugasan: widget.item.idPenugasan, tipeFoto: 'sesudah', foto: _fotoSesudah!); + } else if (_tandaiSelesai) { + // Jika mau selesai, harus ada foto + throw PenugasanApiException(message: "Foto sesudah pekerjaan wajib diisi sebelum selesai."); + } + + if (_tandaiSelesai) { + await widget.api.updateStatus( + idPenugasan: widget.item.idPenugasan, + statusPekerjaan: 'selesai', + tanggalDiselesaikan: DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()), + ); + } else if (_fotoSesudah == null) { + throw PenugasanApiException(message: "Tidak ada aksi yang dilakukan."); + } + + widget.onSuccess(); + } on PenugasanApiException catch (e) { + setState(() => _isLoading = false); widget.onFail(e.message); + } catch (e) { + setState(() => _isLoading = false); widget.onFail('Gagal: $e'); + } + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: DraggableScrollableSheet( + expand: false, initialChildSize: 0.65, maxChildSize: 0.9, + builder: (_, controller) => Container( + decoration: BoxDecoration(color: _bg1, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), border: Border(top: BorderSide(color: _line2))), + child: SingleChildScrollView( + controller: controller, padding: EdgeInsets.all(24), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Center(child: Container(width: 48, height: 5, decoration: BoxDecoration(color: _bg2, borderRadius: BorderRadius.circular(3)))), + SizedBox(height: 24), + Text('Update Progres Pekerjaan', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w800, color: _t1)), + Text('${widget.item.labelJenisPekerjaanFallback} • ${widget.item.totalNilaiFormatted}', style: TextStyle(fontSize: 13, color: _t3)), + SizedBox(height: 24), + + Text('FOTO HASIL PEKERJAAN', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w800, color: _t2, letterSpacing: 0.8)), + SizedBox(height: 10), + GestureDetector( + onTap: _pickFoto, + child: Container( + height: 140, + decoration: BoxDecoration(color: _bg2, borderRadius: BorderRadius.circular(10), border: Border.all(color: _line2)), + child: _fotoSesudah != null + ? kIsWeb + ? Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [Icon(Icons.check_circle, color: _cyan, size: 36), SizedBox(height: 8), Text('Foto dipilih ✓', style: TextStyle(color: _cyan, fontWeight: FontWeight.w600))])) + : ClipRRect(borderRadius: BorderRadius.circular(10), child: Image.file(File(_fotoSesudah!.path), fit: BoxFit.cover, width: double.infinity)) + : Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Container(padding: EdgeInsets.all(10), decoration: BoxDecoration(color: _greenDim, shape: BoxShape.circle), child: Icon(Icons.camera_alt_outlined, color: _green, size: 28)), + SizedBox(height: 10), + Text('Ambil Foto Hasil', style: TextStyle(color: _t2, fontSize: 13, fontWeight: FontWeight.w500)) + ])), + ), + ), + SizedBox(height: 24), + + AnimatedContainer( + duration: Duration(milliseconds: 200), + decoration: BoxDecoration( + color: _tandaiSelesai ? _greenDim : _bg2, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: _tandaiSelesai ? _green.withOpacity(0.4) : _line2), + ), + child: Theme( + data: ThemeData(unselectedWidgetColor: _t3), + child: CheckboxListTile( + value: _tandaiSelesai, + onChanged: (v) => setState(() => _tandaiSelesai = v ?? false), + title: Text('Tandai Pekerjaan Selesai', style: TextStyle(fontWeight: FontWeight.w700, fontSize: 14, color: _tandaiSelesai ? _green : _t1)), + subtitle: Text('Centang jika pekerjaan sudah selesai', style: TextStyle(fontSize: 12, color: _tandaiSelesai ? _green.withOpacity(0.8) : _t2)), + activeColor: _green, checkColor: Colors.black, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + ), + ), + SizedBox(height: 32), + + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _isLoading ? null : _submit, + style: ElevatedButton.styleFrom( + backgroundColor: _tandaiSelesai ? _green : _cyan, + foregroundColor: Colors.black, + padding: EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + child: _isLoading + ? SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.black, strokeWidth: 2)) + : Text(_tandaiSelesai ? 'SIMPAN & TANDAI SELESAI' : 'SIMPAN FOTO', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800, letterSpacing: 1.2)), + ), + ), + SizedBox(height: 20), + ]), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/samooflutter/lib/tugas/Progress.dart b/samooflutter/lib/tugas/Progress.dart new file mode 100644 index 0000000..660f544 --- /dev/null +++ b/samooflutter/lib/tugas/Progress.dart @@ -0,0 +1,1679 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:image_picker/image_picker.dart'; +import 'package:intl/intl.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../models/Penugasan_model.dart'; +import '../api/PenugasanApi.dart'; + +const _bg = Color(0xFFF9FAFB); +const _bg1 = Color(0xFFFFFFFF); +const _bg2 = Color(0xFFF3F4F6); +const _green = Color(0xFF10B981); +const _greenDim = Color(0x1A10B981); +const _cyan = Color(0xFF06B6D4); +const _cyanDim = Color(0x1A06B6D4); +const _amber = Color(0xFFF59E0B); +const _amberDim = Color(0x1AF59E0B); +const _rose = Color(0xFFEF4444); +const _roseDim = Color(0x1AEF4444); +const _t1 = Color(0xFF111827); +const _t2 = Color(0xFF6B7280); +const _t3 = Color(0xFF9CA3AF); +const _line2 = Color(0xFFE5E7EB); + +class ProgressScreen extends StatefulWidget { + @override + _ProgressScreenState createState() => _ProgressScreenState(); +} + +class _ProgressScreenState extends State + with SingleTickerProviderStateMixin { + final _api = PenugasanApi(); + List activeList = []; + List historyList = []; + bool isLoading = false; + String? errorMessage; + late TabController _tabController; + bool? _pakaiPipaBesi; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + _loadData(); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + Future _loadData() async { + setState(() { + isLoading = true; + errorMessage = null; + }); + try { + final results = await Future.wait([ + _fetchAllPages(status: 'dalam_proses'), + _fetchAllPages(status: 'selesai'), + ]); + + if (mounted) { + setState(() { + activeList = results[0]; + historyList = results[1]; + isLoading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + errorMessage = 'Gagal terhubung ke server.'; + isLoading = false; + }); + } + } + } + + Future> _fetchAllPages({required String status}) async { + final List all = []; + int page = 1; + + while (true) { + final result = await _api.getPenugasanList(status: status, page: page); + final data = result['data']; + final items = (data['data'] as List? ?? []); + all.addAll(items.map((e) => PenugasanModel.fromJson(e))); + + final int currentPage = data['current_page'] ?? 1; + final int lastPage = data['last_page'] ?? 1; + if (currentPage >= lastPage) break; + page++; + } + + return all; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: _bg, + appBar: AppBar( + title: Text('Daftar Tugas', + style: TextStyle(fontWeight: FontWeight.w800, fontSize: 16)), + backgroundColor: _bg1, + foregroundColor: _t1, + elevation: 0, + bottom: TabBar( + controller: _tabController, + indicatorColor: _green, + labelColor: _green, + unselectedLabelColor: _t2, + indicatorWeight: 3, + dividerColor: _line2, + tabs: [ + Tab(text: 'Progres Aktif'), + Tab(text: 'Riwayat Selesai'), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + _buildList(activeList, 'Tidak ada pekerjaan berjalan', Icons.trending_up_rounded), + _buildList(historyList, 'Belum ada riwayat tugas', Icons.history_rounded), + ], + ), + ); + } + + Widget _buildList(List list, String emptyMsg, IconData emptyIcon) { + if (isLoading) + return Center(child: CircularProgressIndicator(color: _green)); + if (errorMessage != null) + return Center(child: Text(errorMessage!, style: TextStyle(color: _t1))); + if (list.isEmpty) + return Center( + child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(emptyIcon, size: 60, color: _t3), + SizedBox(height: 12), + Text(emptyMsg, style: TextStyle(fontSize: 14, color: _t2)), + ])); + + return RefreshIndicator( + onRefresh: _loadData, + color: _green, + child: ListView.builder( + padding: EdgeInsets.all(16), + itemCount: list.length, + itemBuilder: (context, index) => _buildProgressCard(list[index]), + ), + ); + } + + Widget _buildProgressCard(PenugasanModel item) { + final bool isDone = item.statusPekerjaan == 'selesai'; + + // ── VALIDASI FOTO: cek apakah foto sebelum & sesudah sudah ada ── + final bool sudahFotoSebelum = item.fotoSebelumUrl != null && + item.fotoSebelumUrl!.isNotEmpty; + final bool sudahFotoSesudah = item.fotoSesudahUrl != null && + item.fotoSesudahUrl!.isNotEmpty; + final bool bolehSelesai = sudahFotoSebelum && sudahFotoSesudah; + + return Container( + margin: EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: _bg1, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isDone + ? _green.withOpacity(0.3) + : _amber.withOpacity(0.3), + ), + ), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + // ── Header ── + Container( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: isDone ? _greenDim : _amberDim, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Row(children: [ + Icon( + isDone ? Icons.check_circle_outline : Icons.timer_outlined, + size: 14, + color: isDone ? _green : _amber, + ), + SizedBox(width: 6), + Text( + isDone ? 'SELESAI' : 'SEDANG DIKERJAKAN', + style: TextStyle( + color: isDone ? _green : _amber, + fontSize: 10, + fontWeight: FontWeight.w900, + letterSpacing: 1), + ), + ]), + Text( + DateFormat('dd MMM yyyy').format(DateTime.parse(item.tanggalDiberikan).toLocal()), + style: TextStyle( + color: (isDone ? _green : _amber).withOpacity(0.7), + fontSize: 11), + ), + ]), + ), + + Padding( + padding: EdgeInsets.all(16), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(item.labelJenisPekerjaanFallback, + style: TextStyle( + fontSize: 16, fontWeight: FontWeight.w800, color: _t1)), + SizedBox(height: 8), + + if (item.teknisi != null) + Row(children: [ + Icon(Icons.person_outline, size: 14, color: _t2), + SizedBox(width: 6), + Expanded( + child: Text(item.namaTim, + style: TextStyle(fontSize: 13, color: _t2))), + ]), + SizedBox(height: 8), + + if (item.alamatLokasi != null) + Container( + padding: EdgeInsets.all(10), + decoration: BoxDecoration( + color: _bg2, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: _line2), + ), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(children: [ + Icon(Icons.location_on_outlined, size: 14, color: _green), + SizedBox(width: 6), + Expanded( + child: Text(item.alamatLokasi!, + style: TextStyle( + fontSize: 13, + color: _t1, + fontWeight: FontWeight.w600))), + ]), + if (item.namaPelanggan != null || item.noSambungan != null) ...[ + SizedBox(height: 6), + Row(children: [ + Icon(Icons.person, size: 13, color: _t2), + SizedBox(width: 6), + Text(item.namaPelanggan ?? '-', + style: TextStyle(fontSize: 12, color: _t2)), + if (item.noSambungan != null) ...[ + SizedBox(width: 10), + Icon(Icons.tag, size: 13, color: _t2), + SizedBox(width: 4), + Text(item.noSambungan!, + style: TextStyle(fontSize: 12, color: _t2)), + ], + ]), + ], + ]), + ), + + if (item.totalNilaiFormatted != 'Rp 0') ...[ + SizedBox(height: 10), + Row(children: [ + Icon(Icons.monetization_on_outlined, size: 14, color: _green), + SizedBox(width: 6), + Text(item.totalNilaiFormatted, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w800, + color: _green)), + ]), + ], + + // ── INDIKATOR STATUS FOTO (hanya untuk tugas aktif) ── + if (!isDone) ...[ + SizedBox(height: 10), + Row(children: [ + _fotoStatusChip( + label: 'Foto Sebelum', + sudah: sudahFotoSebelum, + ), + SizedBox(width: 8), + _fotoStatusChip( + label: 'Foto Sesudah', + sudah: sudahFotoSesudah, + ), + ]), + ], + + SizedBox(height: 14), + Divider(height: 1, color: _line2), + SizedBox(height: 14), + + if (!isDone) + Row(children: [ + Expanded( + child: _aksiBtn( + label: 'Foto Sebelum', + icon: Icons.camera_outlined, + color: _cyan, + onTap: () => _showUploadFotoSheet(item, tipeFoto: 'sebelum'), + )), + SizedBox(width: 8), + Expanded( + child: _aksiBtn( + label: 'Gali Urug', + icon: Icons.construction_rounded, + color: _cyan, + onTap: () => _showAddGaliUrugDialog(item), + )), + SizedBox(width: 8), + Expanded( + child: _aksiBtn( + label: 'Foto Sesudah', + icon: Icons.camera_alt_outlined, + color: _amber, + onTap: () => _showUploadFotoSheet(item, tipeFoto: 'sesudah'), + )), + SizedBox(width: 8), + // ── TOMBOL SELESAI: disabled jika foto belum lengkap ── + Expanded( + child: _aksiBtn( + label: 'Selesai', + icon: Icons.check_circle_outline, + color: _green, + disabled: !bolehSelesai, + onTap: () { + if (!sudahFotoSebelum) { + _showFotoWarningSnackbar('Foto sebelum belum diupload!'); + return; + } + if (!sudahFotoSesudah) { + _showFotoWarningSnackbar('Foto sesudah belum diupload!'); + return; + } + _pakaiPipaBesi = null; + _openFinishSheet(item); + }, + )), + ]) + else + Row(children: [ + Expanded( + child: _aksiBtn( + label: 'Lihat Detail', + icon: Icons.info_outline, + color: _green, + onTap: () => _openDetailSheet(item), + )), + SizedBox(width: 8), + Expanded( + child: _aksiBtn( + label: 'Edit', + icon: Icons.edit_outlined, + color: _cyan, + onTap: () => _openDetailSheet(item, startEditing: true), + )), + ]), + ]), + ), + ]), + ); + } + + // ── Widget chip kecil indikator status foto ── + Widget _fotoStatusChip({required String label, required bool sudah}) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: sudah ? _greenDim : _roseDim, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: sudah ? _green.withOpacity(0.4) : _rose.withOpacity(0.4), + ), + ), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Icon( + sudah ? Icons.check_circle : Icons.radio_button_unchecked, + size: 11, + color: sudah ? _green : _rose, + ), + SizedBox(width: 4), + Text( + label, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + color: sudah ? _green : _rose, + ), + ), + ]), + ); + } + + // ── Snackbar peringatan foto belum diupload ── + void _showFotoWarningSnackbar(String pesan) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Row(children: [ + Icon(Icons.warning_amber_rounded, color: _rose, size: 18), + SizedBox(width: 8), + Expanded(child: Text(pesan, style: TextStyle(color: _t1, fontSize: 13))), + ]), + backgroundColor: _bg1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide(color: _rose.withOpacity(0.4)), + ), + behavior: SnackBarBehavior.floating, + duration: Duration(seconds: 3), + )); + } + + void _openFinishSheet(PenugasanModel item) { + final dimensiCtrl = TextEditingController(); + final jarakCtrl = TextEditingController(); + final unitCtrl = TextEditingController(); + final detailCtrl = TextEditingController(); + bool? localPakaiPipaBesi; + + showModalBottomSheet( + context: context, + backgroundColor: _bg1, + isScrollControlled: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20))), + builder: (sheetCtx) { + return StatefulBuilder( + builder: (ctx, setSheet) { + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(ctx).viewInsets.bottom), + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: _line2, + borderRadius: BorderRadius.circular(10)), + margin: EdgeInsets.only(bottom: 20)), + ), + + Row(children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: _greenDim, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _green.withOpacity(0.3))), + child: Icon(Icons.check_circle_rounded, + size: 22, color: _green), + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Selesaikan Pekerjaan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: _t1)), + Text(item.labelJenisPekerjaanFallback, + style: TextStyle( + fontSize: 12, color: _t2)), + ]), + ), + ]), + SizedBox(height: 16), + + // ── Info: foto sudah lengkap ── + Container( + padding: EdgeInsets.symmetric( + horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: _greenDim, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: _green.withOpacity(0.3))), + child: Row(children: [ + Icon(Icons.check_circle_outline, + size: 15, color: _green), + SizedBox(width: 8), + Expanded( + child: Text( + 'Foto sebelum & sesudah sudah terupload.', + style: TextStyle( + fontSize: 12, color: _green))), + ]), + ), + SizedBox(height: 20), + + Text('Isi Detail Pekerjaan', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: _t1)), + SizedBox(height: 12), + + if (['sr', 'pengembangan_jaringan_pipa', 'pemasangan_gate_valve', 'perbaikan_jaringan_pipa', 'pengecatan_pipa_besi', 'penyempurnaan_jaringan_pipa'].contains(item.jenisPekerjaan)) ...[ + _sheetField( + label: 'Dimensi Pipa', + controller: dimensiCtrl, + hint: 'Cth: 2, 3, 4, 6'), + SizedBox(height: 10), + ], + + if (['pengembangan_jaringan_pipa', 'penyempurnaan_jaringan_pipa'].contains(item.jenisPekerjaan)) ...[ + _sheetField( + label: 'Jarak (meter)', + controller: jarakCtrl, + hint: '0.00', + keyboardType: TextInputType.number), + SizedBox(height: 10), + ], + + if (['sr', 'pengangkatan', 'perbaikan_jaringan_pipa'].contains(item.jenisPekerjaan)) ...[ + _sheetField( + label: 'Jumlah Unit', + controller: unitCtrl, + hint: '0', + keyboardType: TextInputType.number), + SizedBox(height: 10), + ], + + if (item.jenisPekerjaan == 'pemasangan_gate_valve') + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Pakai Pipa Besi?', + style: TextStyle( + fontSize: 12, + color: _t2, + fontWeight: FontWeight.w600)), + SizedBox(height: 8), + Row(children: [ + Expanded( + child: GestureDetector( + onTap: () => + setSheet(() => localPakaiPipaBesi = true), + child: Container( + padding: + EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: localPakaiPipaBesi == true + ? _greenDim + : _bg2, + borderRadius: + BorderRadius.circular(10), + border: Border.all( + color: localPakaiPipaBesi == true + ? _green + : _line2, + width: + localPakaiPipaBesi == true + ? 1.5 + : 1), + ), + child: Text('Ya (Rp 200.000)', + textAlign: TextAlign.center, + style: TextStyle( + color: + localPakaiPipaBesi == true + ? _green + : _t2, + fontSize: 12, + fontWeight: + localPakaiPipaBesi == true + ? FontWeight.w700 + : FontWeight.w400)), + ), + ), + ), + SizedBox(width: 10), + Expanded( + child: GestureDetector( + onTap: () => + setSheet(() => localPakaiPipaBesi = false), + child: Container( + padding: + EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: localPakaiPipaBesi == false + ? _cyanDim + : _bg2, + borderRadius: + BorderRadius.circular(10), + border: Border.all( + color: localPakaiPipaBesi == false + ? _cyan + : _line2, + width: + localPakaiPipaBesi == false + ? 1.5 + : 1), + ), + child: Text('Tidak (Gratis)', + textAlign: TextAlign.center, + style: TextStyle( + color: + localPakaiPipaBesi == false + ? _cyan + : _t2, + fontSize: 12, + fontWeight: + localPakaiPipaBesi == false + ? FontWeight.w700 + : FontWeight.w400)), + ), + ), + ), + ]), + SizedBox(height: 10), + ], + ), + + _sheetField( + label: 'Detail Pekerjaan', + controller: detailCtrl, + hint: 'Deskripsi pekerjaan...', + maxLines: 3), + SizedBox(height: 20), + + Row(children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.pop(sheetCtx), + child: Text('Batal'), + style: OutlinedButton.styleFrom( + foregroundColor: _t2, + side: BorderSide(color: _line2), + padding: EdgeInsets.symmetric(vertical: 14)), + )), + SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () async { + Navigator.pop(sheetCtx); + setState(() => isLoading = true); + + try { + final bool adaDetail = + detailCtrl.text.isNotEmpty || + jarakCtrl.text.isNotEmpty || + unitCtrl.text.isNotEmpty || + dimensiCtrl.text.isNotEmpty || + localPakaiPipaBesi != null; + + // STEP 1: Simpan detail dulu (jika ada) + if (adaDetail) { + await _api.lengkapiDetail( + idPenugasan: item.idPenugasan, + items: [ + { + 'jenis_pekerjaan': + item.jenisPekerjaan ?? '', + 'dimensi_pipa': + dimensiCtrl.text.isEmpty + ? null + : dimensiCtrl.text, + 'jarak_meter': + jarakCtrl.text.isEmpty + ? null + : double.tryParse( + jarakCtrl.text), + 'jumlah_unit': + unitCtrl.text.isEmpty + ? null + : int.tryParse(unitCtrl.text), + 'pakai_pipa_besi': localPakaiPipaBesi, + } + ], + tanggalMulai: item.tanggalDiberikan, + detailPekerjaan: detailCtrl.text, + ); + } + + // STEP 2: Update status ke selesai + await _api.updateStatus( + idPenugasan: item.idPenugasan, + statusPekerjaan: 'selesai', + tanggalDiselesaikan: + DateTime.now().toIso8601String(), + ); + + // STEP 3: Reload data + await _loadData(); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Pekerjaan berhasil diselesaikan! ✓'), + backgroundColor: _bg1, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(10), + side: BorderSide( + color: + _green.withOpacity(0.4))), + behavior: SnackBarBehavior.floating, + ), + ); + _tabController.animateTo(1); + } + } catch (e) { + if (mounted) { + setState(() => isLoading = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Gagal: $e'), + backgroundColor: _bg1, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(10), + side: BorderSide( + color: + _rose.withOpacity(0.4))), + behavior: SnackBarBehavior.floating, + ), + ); + } + } finally { + dimensiCtrl.dispose(); + jarakCtrl.dispose(); + unitCtrl.dispose(); + detailCtrl.dispose(); + } + }, + child: Text('Ya, Selesai', + style: TextStyle( + fontWeight: FontWeight.w800)), + style: ElevatedButton.styleFrom( + backgroundColor: _green, + foregroundColor: Colors.black, + padding: EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(10)), + ), + )), + ]), + SizedBox(height: 8), + ]), + ), + ), + ); + }, + ); + }, + ); + } + + Widget _sheetField({ + required String label, + required TextEditingController controller, + String? hint, + int maxLines = 1, + TextInputType keyboardType = TextInputType.text, + }) { + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(label, + style: TextStyle( + fontSize: 12, color: _t2, fontWeight: FontWeight.w600)), + SizedBox(height: 6), + TextField( + controller: controller, + keyboardType: keyboardType, + maxLines: maxLines, + style: TextStyle(color: _t1, fontSize: 13), + decoration: InputDecoration( + hintText: hint, + hintStyle: TextStyle(color: _t3), + contentPadding: + EdgeInsets.symmetric(horizontal: 12, vertical: 10), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: _line2), + borderRadius: BorderRadius.circular(8)), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: _green), + borderRadius: BorderRadius.circular(8)), + filled: true, + fillColor: _bg2, + ), + ), + ]); + } + + Future _openDetailSheet(PenugasanModel item, + {bool startEditing = false}) async { + await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: _bg1, + builder: (context) => DetailPenugasanSheet( + penugasan: item, + api: _api, + startEditing: startEditing, + onUpdated: _loadData, + ), + ); + } + + // ── _aksiBtn: sekarang mendukung parameter disabled ── + Widget _aksiBtn({ + required String label, + required IconData icon, + required Color color, + required VoidCallback onTap, + bool disabled = false, + }) { + final effectiveColor = disabled ? _t3 : color; + + return GestureDetector( + onTap: disabled ? onTap : onTap, // tetap bisa tap agar snackbar muncul + child: Container( + padding: EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + color: effectiveColor.withOpacity(0.08), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: effectiveColor.withOpacity(0.3)), + ), + child: Column(children: [ + Icon(icon, size: 18, color: effectiveColor), + SizedBox(height: 4), + Text(label, + style: TextStyle( + fontSize: 10, + color: effectiveColor, + fontWeight: FontWeight.w700)), + ]), + ), + ); + } + + void _showUploadFotoSheet(PenugasanModel item, {String tipeFoto = 'sesudah'}) { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (_) => _UploadFotoSheet( + item: item, + api: _api, + tipeFoto: tipeFoto, + onSuccess: () { + Navigator.pop(context); + _loadData(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + 'Foto ${tipeFoto == 'sebelum' ? 'sebelum' : 'sesudah'} berhasil diupload!'), + backgroundColor: _bg1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide(color: _green.withOpacity(0.4))), + behavior: SnackBarBehavior.floating, + )); + }, + onFail: (err) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Gagal: $err'), + backgroundColor: _bg1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide(color: _rose.withOpacity(0.4))), + behavior: SnackBarBehavior.floating, + )); + }, + ), + ); + } + + void _showAddGaliUrugDialog(PenugasanModel item) { + final ctrl = TextEditingController(text: '1'); + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: _bg2, + title: Text('Tambah Titik Gali Urug', + style: TextStyle( + color: _t1, fontSize: 16, fontWeight: FontWeight.w800)), + content: Column(mainAxisSize: MainAxisSize.min, children: [ + Text('Berapa titik gali urug yang ingin ditambahkan?', + style: TextStyle(color: _t2, fontSize: 13)), + SizedBox(height: 16), + TextField( + controller: ctrl, + keyboardType: TextInputType.number, + style: TextStyle(color: _t1), + decoration: InputDecoration( + labelText: 'Jumlah Titik', + labelStyle: TextStyle(color: _cyan), + suffixText: 'titik', + suffixStyle: TextStyle(color: _t2), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: _line2), + borderRadius: BorderRadius.circular(10)), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: _cyan), + borderRadius: BorderRadius.circular(10)), + filled: true, + fillColor: _bg1, + ), + ), + ]), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text('Batal', style: TextStyle(color: _t3))), + ElevatedButton( + onPressed: () async { + final qty = int.tryParse(ctrl.text) ?? 1; + Navigator.pop(context); + setState(() => isLoading = true); + try { + await _api.addItem( + idPenugasan: item.idPenugasan, + item: { + 'jenis_pekerjaan': 'gali_urug', + 'jumlah_titik': qty + }); + await _loadData(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('$qty titik Gali Urug berhasil ditambahkan!'), + backgroundColor: _bg1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide(color: _cyan.withOpacity(0.4))), + behavior: SnackBarBehavior.floating, + )); + } catch (e) { + setState(() => isLoading = false); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Gagal: $e'), + backgroundColor: _bg1)); + } + }, + child: Text('Tambah'), + style: ElevatedButton.styleFrom( + backgroundColor: _cyan, foregroundColor: Colors.black), + ), + ], + ), + ); + } +} + +// =================================== +// UPLOAD FOTO SHEET (Sebelum & Sesudah) +// =================================== +class _UploadFotoSheet extends StatefulWidget { + final PenugasanModel item; + final PenugasanApi api; + final VoidCallback onSuccess; + final Function(String) onFail; + final String tipeFoto; + + const _UploadFotoSheet({ + required this.item, + required this.api, + required this.onSuccess, + required this.onFail, + this.tipeFoto = 'sesudah', + }); + + @override + __UploadFotoSheetState createState() => __UploadFotoSheetState(); +} + +class __UploadFotoSheetState extends State<_UploadFotoSheet> { + XFile? _foto; + bool _isLoading = false; + final _picker = ImagePicker(); + + Future _pickFoto() async { + final picked = + await _picker.pickImage(source: ImageSource.camera, imageQuality: 70); + if (picked != null) setState(() => _foto = picked); + } + + Future _submit() async { + if (_foto == null) { + widget.onFail('Pilih foto terlebih dahulu'); + return; + } + setState(() => _isLoading = true); + try { + await widget.api.uploadFoto( + idPenugasan: widget.item.idPenugasan, + tipeFoto: widget.tipeFoto, + foto: _foto!); + widget.onSuccess(); + } catch (e) { + setState(() => _isLoading = false); + widget.onFail('$e'); + } + } + + @override + Widget build(BuildContext context) { + final bool isSebelum = widget.tipeFoto == 'sebelum'; + final Color accentColor = isSebelum ? _cyan : _amber; + + return DraggableScrollableSheet( + expand: false, + initialChildSize: 0.55, + maxChildSize: 0.8, + builder: (_, controller) => Container( + decoration: BoxDecoration( + color: _bg1, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + border: Border(top: BorderSide(color: _line2))), + padding: EdgeInsets.all(24), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Center( + child: Container( + width: 48, + height: 5, + decoration: BoxDecoration( + color: _bg2, borderRadius: BorderRadius.circular(3)))), + SizedBox(height: 20), + Text( + 'Upload Foto ${isSebelum ? 'Sebelum' : 'Sesudah'}', + style: TextStyle( + fontSize: 17, fontWeight: FontWeight.w800, color: _t1), + ), + Text(widget.item.labelJenisPekerjaanFallback, + style: TextStyle(fontSize: 13, color: _t3)), + SizedBox(height: 20), + GestureDetector( + onTap: _pickFoto, + child: Container( + height: 150, + decoration: BoxDecoration( + color: _bg2, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: _line2)), + child: _foto != null + ? (kIsWeb + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.check_circle, + color: _green, size: 40), + SizedBox(height: 8), + Text('Foto dipilih ✓', + style: TextStyle( + color: _green, + fontWeight: FontWeight.w600)), + ])) + : ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.file(File(_foto!.path), + fit: BoxFit.cover, + width: double.infinity))) + : Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: isSebelum ? _cyanDim : _amberDim, + shape: BoxShape.circle), + child: Icon( + isSebelum + ? Icons.camera_outlined + : Icons.camera_alt_outlined, + color: accentColor, + size: 26)), + SizedBox(height: 10), + Text('Klik untuk ambil foto', + style: TextStyle(color: _t2, fontSize: 13)), + SizedBox(height: 4), + Text( + isSebelum + ? 'Foto kondisi sebelum pengerjaan' + : 'Foto kondisi sesudah pengerjaan', + style: TextStyle(color: _t3, fontSize: 11), + ), + ])), + ), + ), + SizedBox(height: 20), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _isLoading ? null : _submit, + style: ElevatedButton.styleFrom( + backgroundColor: accentColor, + foregroundColor: Colors.black, + padding: EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12))), + child: _isLoading + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: Colors.black, strokeWidth: 2)) + : Text( + 'UPLOAD FOTO ${isSebelum ? 'SEBELUM' : 'SESUDAH'}', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w800, + letterSpacing: 1)), + ), + ), + ]), + ), + ); + } +} + +// =================================== +// DETAIL PENUGASAN SHEET (tugas selesai) +// =================================== +class DetailPenugasanSheet extends StatefulWidget { + final PenugasanModel penugasan; + final PenugasanApi api; + final VoidCallback? onUpdated; + final bool startEditing; + const DetailPenugasanSheet( + {required this.penugasan, + required this.api, + this.onUpdated, + this.startEditing = false}); + @override + _DetailPenugasanSheetState createState() => _DetailPenugasanSheetState(); +} + +class _DetailPenugasanSheetState extends State { + bool _isLoading = false; + bool _isEditing = false; + Map? _detail; + + late TextEditingController _detailCtrl; + late TextEditingController _dimensiCtrl; + late TextEditingController _jarakCtrl; + late TextEditingController _unitCtrl; + + @override + void initState() { + super.initState(); + _detailCtrl = TextEditingController(); + _dimensiCtrl = TextEditingController(); + _jarakCtrl = TextEditingController(); + _unitCtrl = TextEditingController(); + _loadDetail(); + _isEditing = widget.startEditing; + } + + @override + void dispose() { + _detailCtrl.dispose(); + _dimensiCtrl.dispose(); + _jarakCtrl.dispose(); + _unitCtrl.dispose(); + super.dispose(); + } + + Future _loadDetail() async { + setState(() => _isLoading = true); + try { + final result = + await widget.api.getPenugasanDetail(widget.penugasan.idPenugasan); + final detail = result['data'] as Map?; + if (mounted) { + setState(() { + _detail = detail; + _isLoading = false; + _populateForm(); + }); + } + } catch (e) { + if (mounted) setState(() => _isLoading = false); + } + } + + void _populateForm() { + if (_detail == null) return; + final List? itemsList = _detail!['items'] as List?; + final Map? firstItem = + (itemsList != null && itemsList.isNotEmpty) + ? Map.from(itemsList[0]) + : null; + + _detailCtrl.text = _detail!['detail_pekerjaan'] ?? ''; + _dimensiCtrl.text = + (_detail!['dimensi_pipa'] ?? firstItem?['dimensi_pipa'])?.toString() ?? ''; + _jarakCtrl.text = + (_detail!['jarak_meter'] ?? firstItem?['jarak_meter'])?.toString() ?? ''; + _unitCtrl.text = + (_detail!['jumlah_unit'] ?? firstItem?['jumlah_unit'])?.toString() ?? ''; + } + + Future _submitEdit() async { + setState(() => _isLoading = true); + try { + final List? itemsList = _detail!['items'] as List?; + final Map? firstItem = + (itemsList != null && itemsList.isNotEmpty) + ? Map.from(itemsList[0]) + : null; + + final int? idPenugasanItem = firstItem != null + ? (firstItem['id_penugasan_item'] as num?)?.toInt() + : null; + + final String jenisPekerjaan = + (_detail!['jenis_pekerjaan'] ?? firstItem?['jenis_pekerjaan'] ?? '') + .toString(); + + final pakaiPipaBesi = + _detail!['pakai_pipa_besi'] ?? firstItem?['pakai_pipa_besi']; + + final bool showDimensi = ['sr', 'pengembangan_jaringan_pipa', 'pemasangan_gate_valve', 'perbaikan_jaringan_pipa', 'pengecatan_pipa_besi', 'penyempurnaan_jaringan_pipa'].contains(jenisPekerjaan); + final bool showJarak = ['pengembangan_jaringan_pipa', 'penyempurnaan_jaringan_pipa'].contains(jenisPekerjaan); + final bool showUnit = ['sr', 'pengangkatan', 'perbaikan_jaringan_pipa'].contains(jenisPekerjaan); + + final items = [ + { + if (idPenugasanItem != null) 'id_penugasan_item': idPenugasanItem, + 'jenis_pekerjaan': jenisPekerjaan, + 'dimensi_pipa': + (!showDimensi || _dimensiCtrl.text.isEmpty) ? null : _dimensiCtrl.text, + 'jarak_meter': (!showJarak || _jarakCtrl.text.isEmpty) + ? null + : double.tryParse(_jarakCtrl.text), + 'jumlah_unit': (!showUnit || _unitCtrl.text.isEmpty) + ? null + : int.tryParse(_unitCtrl.text), + 'pakai_pipa_besi': pakaiPipaBesi, + } + ]; + + final prefs = await SharedPreferences.getInstance(); + final idTeknisi = prefs.getInt('id_teknisi') ?? 0; + + await widget.api.updateDetail( + idPenugasan: widget.penugasan.idPenugasan, + idTeknisi: idTeknisi, + items: items, + detailPekerjaan: _detailCtrl.text, + tanggalMulai: (_detail!['tanggal_mulai'] ?? + _detail!['tanggal_diberikan'] ?? + DateTime.now().toIso8601String()) + .toString(), + ); + + if (mounted) { + setState(() { + _isLoading = false; + _isEditing = false; + }); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Data berhasil diperbarui!'), + backgroundColor: _green, + behavior: SnackBarBehavior.floating, + )); + widget.onUpdated?.call(); + await _loadDetail(); + } + } catch (e) { + if (mounted) setState(() => _isLoading = false); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Gagal update: $e'), + backgroundColor: _rose, + behavior: SnackBarBehavior.floating)); + } + } + + double? _toDouble(dynamic val) { + if (val == null) return null; + if (val is double) return val; + if (val is int) return val.toDouble(); + if (val is String) return double.tryParse(val); + return null; + } + + bool _hasValue(dynamic val) { + final num = _toDouble(val); + return num != null && num != 0; + } + + dynamic _getField(String key) { + if (_detail == null) return null; + if (_detail![key] != null) return _detail![key]; + final List? itemsList = _detail!['items'] as List?; + if (itemsList != null && itemsList.isNotEmpty) { + return itemsList[0][key]; + } + return null; + } + + Widget _buildPipaBesiToggle() { + return StatefulBuilder(builder: (ctx, setInner) { + final List? itemsList = _detail!['items'] as List?; + final firstItem = (itemsList != null && itemsList.isNotEmpty) + ? Map.from(itemsList[0]) + : null; + final pakai = + _detail!['pakai_pipa_besi'] ?? firstItem?['pakai_pipa_besi']; + + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Pakai Pipa Besi?', + style: TextStyle( + fontSize: 12, color: _t2, fontWeight: FontWeight.w600)), + SizedBox(height: 8), + Row(children: [ + Expanded( + child: GestureDetector( + onTap: () => setInner(() { + _detail!['pakai_pipa_besi'] = true; + if (firstItem != null && + itemsList != null && + itemsList.isNotEmpty) { + (itemsList[0] as Map)['pakai_pipa_besi'] = true; + } + }), + child: Container( + padding: EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: pakai == true ? _greenDim : _bg2, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: pakai == true ? _green : _line2, + width: pakai == true ? 1.5 : 1), + ), + child: Text('Ya (Rp 200.000)', + textAlign: TextAlign.center, + style: TextStyle( + color: pakai == true ? _green : _t2, + fontSize: 12, + fontWeight: pakai == true + ? FontWeight.w700 + : FontWeight.w400)), + ), + ), + ), + SizedBox(width: 10), + Expanded( + child: GestureDetector( + onTap: () => setInner(() { + _detail!['pakai_pipa_besi'] = false; + if (firstItem != null && + itemsList != null && + itemsList.isNotEmpty) { + (itemsList[0] as Map)['pakai_pipa_besi'] = false; + } + }), + child: Container( + padding: EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: pakai == false ? _cyanDim : _bg2, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: pakai == false ? _cyan : _line2, + width: pakai == false ? 1.5 : 1), + ), + child: Text('Tidak (Gratis)', + textAlign: TextAlign.center, + style: TextStyle( + color: pakai == false ? _cyan : _t2, + fontSize: 12, + fontWeight: pakai == false + ? FontWeight.w700 + : FontWeight.w400)), + ), + ), + ), + ]), + SizedBox(height: 12), + ]); + }); + } + + @override + Widget build(BuildContext context) { + return DraggableScrollableSheet( + expand: false, + initialChildSize: 0.7, + maxChildSize: 0.9, + builder: (_, controller) => Container( + decoration: BoxDecoration( + color: _bg1, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + border: Border(top: BorderSide(color: _line2))), + child: Column(children: [ + Padding( + padding: EdgeInsets.all(20), + child: Center( + child: Container( + width: 48, + height: 5, + decoration: BoxDecoration( + color: _bg2, + borderRadius: BorderRadius.circular(3)))), + ), + Expanded( + child: _isLoading + ? Center(child: CircularProgressIndicator(color: _green)) + : _detail == null + ? Center( + child: Text('Gagal memuat detail', + style: TextStyle(color: _t2))) + : ListView( + controller: controller, + padding: EdgeInsets.all(20), + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text('Detail Tugas Selesai', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w800, + color: _t1)), + TextButton( + onPressed: () { + if (_isEditing) { + setState(() { + _isEditing = false; + _populateForm(); + }); + } else { + setState(() => _isEditing = true); + } + }, + child: Text( + _isEditing ? 'Batal' : 'Edit', + style: TextStyle( + color: _green, + fontWeight: FontWeight.w700)), + ), + ]), + SizedBox(height: 20), + + if (_isEditing) ...[ + _buildTextField('Detail Pekerjaan', _detailCtrl, + maxLines: 4), + SizedBox(height: 12), + if (['sr', 'pengembangan_jaringan_pipa', 'pemasangan_gate_valve', 'perbaikan_jaringan_pipa', 'pengecatan_pipa_besi', 'penyempurnaan_jaringan_pipa'].contains(_getField('jenis_pekerjaan') ?? '')) ...[ + _buildTextField('Dimensi Pipa', _dimensiCtrl, + hint: 'Cth: 2, 3, 4, 6'), + SizedBox(height: 12), + ], + if (['pengembangan_jaringan_pipa', 'penyempurnaan_jaringan_pipa'].contains(_getField('jenis_pekerjaan') ?? '')) ...[ + _buildTextField( + 'Jarak (m)', _jarakCtrl, + keyboardType: TextInputType.number), + SizedBox(height: 12), + ], + if (['sr', 'pengangkatan', 'perbaikan_jaringan_pipa'].contains(_getField('jenis_pekerjaan') ?? '')) ...[ + _buildTextField( + 'Jumlah Unit', _unitCtrl, + keyboardType: TextInputType.number), + SizedBox(height: 12), + ], + SizedBox(height: 12), + if ((_getField('jenis_pekerjaan') ?? '') == + 'pemasangan_gate_valve') + _buildPipaBesiToggle(), + SizedBox(height: 4), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: + _isLoading ? null : _submitEdit, + style: ElevatedButton.styleFrom( + backgroundColor: _green, + foregroundColor: Colors.black, + padding: EdgeInsets.symmetric( + vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12))), + child: _isLoading + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: Colors.black, + strokeWidth: 2)) + : Text('Simpan Perubahan', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w800, + letterSpacing: 1)), + ), + ), + SizedBox(height: 18), + Divider(color: _line2), + SizedBox(height: 18), + ], + + _buildRow('Jenis Pekerjaan', + (_detail!['label_jenis_pekerjaan'] ?? + _getField('label_jenis_pekerjaan') ?? + 'N/A') + .toString()), + _buildRow('Lokasi', + (_detail!['alamat_lokasi'] ?? 'N/A').toString()), + _buildRow('Pelanggan', + (_detail!['nama_pelanggan'] ?? '-').toString()), + _buildRow('No. Sambungan', + (_detail!['no_sambungan'] ?? '-').toString()), + + if (_detail!['items'] != null && (_detail!['items'] as List).length > 1) ...[ + SizedBox(height: 16), + Divider(color: _line2), + SizedBox(height: 10), + Text('RINCIAN PEKERJAAN', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w800, color: _cyan, letterSpacing: 1.2)), + SizedBox(height: 12), + ...(_detail!['items'] as List).map((it) => Container( + margin: EdgeInsets.only(bottom: 10), + padding: EdgeInsets.all(12), + decoration: BoxDecoration(color: _bg2, borderRadius: BorderRadius.circular(10), border: Border.all(color: _line2)), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(it['label_jenis_pekerjaan'] ?? 'Pekerjaan', style: TextStyle(color: _t1, fontWeight: FontWeight.bold, fontSize: 13)), + if (it['dimensi_pipa'] != null) Text('Dimensi: ${it['dimensi_pipa']}', style: TextStyle(color: _t2, fontSize: 12)), + if (it['jarak_meter'] != null) Text('Jarak: ${it['jarak_meter']} m', style: TextStyle(color: _t2, fontSize: 12)), + if (it['jumlah_unit'] != null) Text('Jumlah: ${it['jumlah_unit']} unit', style: TextStyle(color: _t2, fontSize: 12)), + if (it['jumlah_titik'] != null) Text('Titik: ${it['jumlah_titik']}', style: TextStyle(color: _t2, fontSize: 12)), + ]), + )).toList(), + ], + + if ((_getField('dimensi_pipa')) + ?.toString() + .isNotEmpty == + true && (_detail!['items'] as List).length <= 1) + _buildRow('Dimensi Pipa', + _getField('dimensi_pipa').toString()), + + _buildRow( + 'Tanggal Diberikan', + DateFormat('dd MMM yyyy').format(DateTime.parse( + (_detail!['tanggal_diberikan'] ?? + DateTime.now().toString()) + .toString()).toLocal())), + _buildRow( + 'Tanggal Selesai', + _detail!['tanggal_diselesaikan'] != null + ? DateFormat('dd MMM yyyy').format( + DateTime.parse(_detail![ + 'tanggal_diselesaikan'] + .toString()).toLocal()) + : '-'), + + if (_detail!['total_nilai_pekerjaan'] != null && + _toDouble( + _detail!['total_nilai_pekerjaan']) != + null && + _toDouble(_detail!['total_nilai_pekerjaan'])! > + 0) + _buildRow( + 'Nilai Pekerjaan', + 'Rp ${NumberFormat('#,###', 'id_ID').format(_toDouble(_detail!['total_nilai_pekerjaan']) ?? 0)}', + color: _green), + + if (_detail!['foto_sebelum_url'] != null || _detail!['foto_sesudah_url'] != null) ...[ + SizedBox(height: 16), + Divider(color: _line2), + SizedBox(height: 16), + Text('FOTO BUKTI', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w800, color: _t3, letterSpacing: 1.2)), + SizedBox(height: 12), + Row(children: [ + if (_detail!['foto_sebelum_url'] != null) Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Sebelum', style: TextStyle(fontSize: 12, color: _t2)), + SizedBox(height: 8), + ClipRRect(borderRadius: BorderRadius.circular(10), child: Image.network(_detail!['foto_sebelum_url'], height: 120, fit: BoxFit.cover, width: double.infinity, + errorBuilder: (_, __, ___) => Container(height: 120, color: _bg2, child: Center(child: Icon(Icons.image_not_supported_outlined, color: _t3))))), + ])), + if (_detail!['foto_sebelum_url'] != null && _detail!['foto_sesudah_url'] != null) SizedBox(width: 16), + if (_detail!['foto_sesudah_url'] != null) Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Sesudah', style: TextStyle(fontSize: 12, color: _t2)), + SizedBox(height: 8), + ClipRRect(borderRadius: BorderRadius.circular(10), child: Image.network(_detail!['foto_sesudah_url'], height: 120, fit: BoxFit.cover, width: double.infinity, + errorBuilder: (_, __, ___) => Container(height: 120, color: _bg2, child: Center(child: Icon(Icons.image_not_supported_outlined, color: _t3))))), + ])), + ]), + ], + + if (_detail!['detail_pekerjaan'] != null && + !_isEditing) ...[ + SizedBox(height: 16), + Divider(color: _line2), + SizedBox(height: 16), + Text('Deskripsi Detail', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: _t1)), + SizedBox(height: 10), + Text( + _detail!['detail_pekerjaan'].toString(), + style: TextStyle( + fontSize: 13, + color: _t2, + height: 1.5)), + ], + ], + ), + ), + ]), + ), + ); + } + + Widget _buildRow(String label, String value, {Color color = _t1}) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 10), + child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox( + width: 120, + child: Text(label, + style: TextStyle( + fontSize: 13, + color: _t3, + fontWeight: FontWeight.w600))), + Expanded( + child: Text(value, + style: TextStyle( + fontSize: 13, + color: color, + fontWeight: FontWeight.w500), + maxLines: 2, + overflow: TextOverflow.ellipsis)), + ]), + ); + } + + Widget _buildTextField( + String label, + TextEditingController controller, { + int maxLines = 1, + String hint = '', + TextInputType keyboardType = TextInputType.text, + }) { + return TextField( + controller: controller, + keyboardType: keyboardType, + maxLines: maxLines, + style: TextStyle(color: _t1), + decoration: InputDecoration( + labelText: label, + hintText: hint.isNotEmpty ? hint : null, + hintStyle: TextStyle(color: _t3), + labelStyle: TextStyle(color: _green), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: _line2), + borderRadius: BorderRadius.circular(12)), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: _green), + borderRadius: BorderRadius.circular(12)), + filled: true, + fillColor: _bg2, + ), + ); + } +} \ No newline at end of file diff --git a/samooflutter/linux/.gitignore b/samooflutter/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/samooflutter/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/samooflutter/linux/CMakeLists.txt b/samooflutter/linux/CMakeLists.txt new file mode 100644 index 0000000..900e421 --- /dev/null +++ b/samooflutter/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "flutter_application_1") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.flutter_application_1") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/samooflutter/linux/flutter/CMakeLists.txt b/samooflutter/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/samooflutter/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/samooflutter/linux/flutter/generated_plugin_registrant.cc b/samooflutter/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..64a0ece --- /dev/null +++ b/samooflutter/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); +} diff --git a/samooflutter/linux/flutter/generated_plugin_registrant.h b/samooflutter/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/samooflutter/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/samooflutter/linux/flutter/generated_plugins.cmake b/samooflutter/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..2db3c22 --- /dev/null +++ b/samooflutter/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/samooflutter/linux/runner/CMakeLists.txt b/samooflutter/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/samooflutter/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/samooflutter/linux/runner/main.cc b/samooflutter/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/samooflutter/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/samooflutter/linux/runner/my_application.cc b/samooflutter/linux/runner/my_application.cc new file mode 100644 index 0000000..98b2273 --- /dev/null +++ b/samooflutter/linux/runner/my_application.cc @@ -0,0 +1,130 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "flutter_application_1"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "flutter_application_1"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/samooflutter/linux/runner/my_application.h b/samooflutter/linux/runner/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/samooflutter/linux/runner/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/samooflutter/macos/.gitignore b/samooflutter/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/samooflutter/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/samooflutter/macos/Flutter/Flutter-Debug.xcconfig b/samooflutter/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/samooflutter/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/samooflutter/macos/Flutter/Flutter-Release.xcconfig b/samooflutter/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/samooflutter/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/samooflutter/macos/Flutter/GeneratedPluginRegistrant.swift b/samooflutter/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..82385d0 --- /dev/null +++ b/samooflutter/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,20 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import file_selector_macos +import geolocator_apple +import path_provider_foundation +import shared_preferences_foundation +import sqflite_darwin + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) +} diff --git a/samooflutter/macos/Runner.xcodeproj/project.pbxproj b/samooflutter/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..2e7fb95 --- /dev/null +++ b/samooflutter/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* flutter_application_1.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "flutter_application_1.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* flutter_application_1.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* flutter_application_1.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterApplication1.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_application_1.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_application_1"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterApplication1.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_application_1.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_application_1"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterApplication1.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_application_1.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_application_1"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/samooflutter/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/samooflutter/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/samooflutter/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/samooflutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/samooflutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..5f39897 --- /dev/null +++ b/samooflutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samooflutter/macos/Runner.xcworkspace/contents.xcworkspacedata b/samooflutter/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/samooflutter/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/samooflutter/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/samooflutter/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/samooflutter/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/samooflutter/macos/Runner/AppDelegate.swift b/samooflutter/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/samooflutter/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/samooflutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/samooflutter/macos/Runner/Base.lproj/MainMenu.xib b/samooflutter/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/samooflutter/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samooflutter/macos/Runner/Configs/AppInfo.xcconfig b/samooflutter/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..e5b4e71 --- /dev/null +++ b/samooflutter/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = flutter_application_1 + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterApplication1 + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/samooflutter/macos/Runner/Configs/Debug.xcconfig b/samooflutter/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/samooflutter/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/samooflutter/macos/Runner/Configs/Release.xcconfig b/samooflutter/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/samooflutter/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/samooflutter/macos/Runner/Configs/Warnings.xcconfig b/samooflutter/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/samooflutter/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/samooflutter/macos/Runner/DebugProfile.entitlements b/samooflutter/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/samooflutter/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/samooflutter/macos/Runner/Info.plist b/samooflutter/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/samooflutter/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/samooflutter/macos/Runner/MainFlutterWindow.swift b/samooflutter/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/samooflutter/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/samooflutter/macos/Runner/Release.entitlements b/samooflutter/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/samooflutter/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/samooflutter/macos/RunnerTests/RunnerTests.swift b/samooflutter/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/samooflutter/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/samooflutter/pubspec.lock b/samooflutter/pubspec.lock new file mode 100644 index 0000000..221cfff --- /dev/null +++ b/samooflutter/pubspec.lock @@ -0,0 +1,895 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "942a4791cd385a68ccb3b32c71c427aba508a1bb949b86dff2adbe4049f16239" + url: "https://pub.dev" + source: hosted + version: "0.3.5" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + device_frame: + dependency: transitive + description: + name: device_frame + sha256: "7b2ebb2a09d6cc0f086b51bd1412d7be83e0170056a7290349169be41164c86a" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + device_preview: + dependency: "direct main" + description: + name: device_preview + sha256: "88aa1cc73ee9a8ec771b309dcbc4000cc66b5d8456b825980997640ab1195bf5" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c" + url: "https://pub.dev" + source: hosted + version: "0.9.4+4" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" + url: "https://pub.dev" + source: hosted + version: "0.9.3+4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: c2fe1001710127dfa7da89977a08d591398370d099aacdaa6d44da7eb14b8476 + url: "https://pub.dev" + source: hosted + version: "2.0.31" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + geocoding: + dependency: "direct main" + description: + name: geocoding + sha256: "790eea732b22a08dd36fc3761bcd29040461ac20ece4d165264a6c0b5338f115" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + geocoding_android: + dependency: transitive + description: + name: geocoding_android + sha256: "1b13eca79b11c497c434678fed109c2be020b158cec7512c848c102bc7232603" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + geocoding_ios: + dependency: transitive + description: + name: geocoding_ios + sha256: "8a39bfb650af55209c42e564036a550b32d029e0733af01dc66c5afea50388d3" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + geocoding_platform_interface: + dependency: transitive + description: + name: geocoding_platform_interface + sha256: "8c2c8226e5c276594c2e18bfe88b19110ed770aeb7c1ab50ede570be8b92229b" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: f4efb8d3c4cdcad2e226af9661eb1a0dd38c71a9494b22526f9da80ab79520e5 + url: "https://pub.dev" + source: hosted + version: "10.1.1" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d + url: "https://pub.dev" + source: hosted + version: "4.6.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.dev" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: "102e7da05b48ca6bf0a5bda0010f886b171d1a08059f01bfe02addd0175ebece" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" + http: + dependency: "direct main" + description: + name: http + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + url: "https://pub.dev" + source: hosted + version: "1.5.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e" + url: "https://pub.dev" + source: hosted + version: "0.8.13+1" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e + url: "https://pub.dev" + source: hosted + version: "0.8.13" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04 + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + url: "https://pub.dev" + source: hosted + version: "10.0.9" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37" + url: "https://pub.dev" + source: hosted + version: "2.2.19" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" + url: "https://pub.dev" + source: hosted + version: "11.4.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + url: "https://pub.dev" + source: hosted + version: "12.1.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + provider: + dependency: transitive + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74 + url: "https://pub.dev" + source: hosted + version: "2.4.12" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + timezone: + dependency: "direct main" + description: + name: timezone + sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.dev" + source: hosted + version: "4.5.2" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + url: "https://pub.dev" + source: hosted + version: "15.0.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" +sdks: + dart: ">=3.8.1 <4.0.0" + flutter: ">=3.32.0" diff --git a/samooflutter/pubspec.yaml b/samooflutter/pubspec.yaml new file mode 100644 index 0000000..7d89fc8 --- /dev/null +++ b/samooflutter/pubspec.yaml @@ -0,0 +1,104 @@ +name: flutter_application_1 +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.8.1 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + http: ^1.1.0 + shared_preferences: ^2.2.2 + image_picker: ^1.0.7 + geolocator: ^10.1.0 + geocoding: ^2.1.1 + intl: ^0.20.2 + device_preview: ^1.1.0 + path_provider: ^2.1.1 + timezone: ^0.9.0 + flutter_svg: ^2.0.9 + permission_handler: ^11.3.1 + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + cached_network_image: ^3.4.1 + +dev_dependencies: + flutter_test: + sdk: flutter + device_preview: ^1.1.0 + + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/samooflutter/test/widget_test.dart b/samooflutter/test/widget_test.dart new file mode 100644 index 0000000..2808800 --- /dev/null +++ b/samooflutter/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:flutter_application_1/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/samooflutter/web/favicon.png b/samooflutter/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/samooflutter/web/favicon.png differ diff --git a/samooflutter/web/icons/Icon-192.png b/samooflutter/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/samooflutter/web/icons/Icon-192.png differ diff --git a/samooflutter/web/icons/Icon-512.png b/samooflutter/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/samooflutter/web/icons/Icon-512.png differ diff --git a/samooflutter/web/icons/Icon-maskable-192.png b/samooflutter/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/samooflutter/web/icons/Icon-maskable-192.png differ diff --git a/samooflutter/web/icons/Icon-maskable-512.png b/samooflutter/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/samooflutter/web/icons/Icon-maskable-512.png differ diff --git a/samooflutter/web/index.html b/samooflutter/web/index.html new file mode 100644 index 0000000..57492ac --- /dev/null +++ b/samooflutter/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + PDAM Teknisi + + + + + + diff --git a/samooflutter/web/manifest.json b/samooflutter/web/manifest.json new file mode 100644 index 0000000..f7cf53b --- /dev/null +++ b/samooflutter/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "PDAM Teknisi", + "short_name": "PDAM Teknisi", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/samooflutter/windows/.gitignore b/samooflutter/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/samooflutter/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/samooflutter/windows/CMakeLists.txt b/samooflutter/windows/CMakeLists.txt new file mode 100644 index 0000000..8cc860b --- /dev/null +++ b/samooflutter/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(flutter_application_1 LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "flutter_application_1") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/samooflutter/windows/flutter/CMakeLists.txt b/samooflutter/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/samooflutter/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/samooflutter/windows/flutter/generated_plugin_registrant.cc b/samooflutter/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..921279f --- /dev/null +++ b/samooflutter/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,20 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + GeolocatorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GeolocatorWindows")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); +} diff --git a/samooflutter/windows/flutter/generated_plugin_registrant.h b/samooflutter/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/samooflutter/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/samooflutter/windows/flutter/generated_plugins.cmake b/samooflutter/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..71dd257 --- /dev/null +++ b/samooflutter/windows/flutter/generated_plugins.cmake @@ -0,0 +1,26 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows + geolocator_windows + permission_handler_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/samooflutter/windows/runner/CMakeLists.txt b/samooflutter/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/samooflutter/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/samooflutter/windows/runner/Runner.rc b/samooflutter/windows/runner/Runner.rc new file mode 100644 index 0000000..765011b --- /dev/null +++ b/samooflutter/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "flutter_application_1" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "flutter_application_1" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "flutter_application_1.exe" "\0" + VALUE "ProductName", "flutter_application_1" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/samooflutter/windows/runner/flutter_window.cpp b/samooflutter/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/samooflutter/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/samooflutter/windows/runner/flutter_window.h b/samooflutter/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/samooflutter/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/samooflutter/windows/runner/main.cpp b/samooflutter/windows/runner/main.cpp new file mode 100644 index 0000000..bb23060 --- /dev/null +++ b/samooflutter/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"flutter_application_1", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/samooflutter/windows/runner/resource.h b/samooflutter/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/samooflutter/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/samooflutter/windows/runner/resources/app_icon.ico b/samooflutter/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/samooflutter/windows/runner/resources/app_icon.ico differ diff --git a/samooflutter/windows/runner/runner.exe.manifest b/samooflutter/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/samooflutter/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/samooflutter/windows/runner/utils.cpp b/samooflutter/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/samooflutter/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/samooflutter/windows/runner/utils.h b/samooflutter/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/samooflutter/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/samooflutter/windows/runner/win32_window.cpp b/samooflutter/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/samooflutter/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/samooflutter/windows/runner/win32_window.h b/samooflutter/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/samooflutter/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_