From 266632ec35f3559e7999ff317c0ced54e37b98c0 Mon Sep 17 00:00:00 2001 From: marszayn Date: Mon, 21 Jul 2025 08:53:14 +0700 Subject: [PATCH] initial commit --- .gitignore | 5 + CHANGES_TERAKHIR_TERBIT.md | 75 ++++ FASTEST_INTEGRATION.md | 150 ++++++++ FINAL_OPTIMIZATION_SUMMARY.md | 191 ++++++++++ PERIZINAN_API_SETUP.md | 164 ++++++++ PRODUCTION_DEPLOY.md | 191 ++++++++++ TASK_SCHEDULER_SETUP.md | 73 ++++ app/Console/Commands/CheckPerizinanData.php | 38 ++ app/Console/Commands/SyncPerizinanData.php | 154 ++++++++ app/Console/Commands/TestProductionSetup.php | 122 ++++++ app/Helpers/DashboardHelper.php | 248 +++++++++++- app/Http/Controllers/DashboardController.php | 15 +- app/Models/FastestPermohonan.php | 54 +++ app/Models/PerizinanStatus.php | 50 +++ app/Models/TerakhirTerbit.php | 50 +++ app/Services/PerizinanApiService.php | 358 ++++++++++++++++++ check-environment.sh | 77 ++++ check_fastest_data.php | 29 ++ check_terakhir_terbit.php | 29 ++ config/services.php | 6 + ...4_015824_create_perizinan_status_table.php | 37 ++ ...024445_create_fastest_permohonan_table.php | 38 ++ ...14_033547_create_terakhir_terbit_table.php | 37 ++ debug_api.php | 22 ++ deploy-production.sh | 109 ++++++ resources/views/dashboard/index.blade.php | 137 ++++++- routes/console.php | 8 + run-scheduler.bat | 8 + run-scheduler.sh | 13 + setup-task-scheduler.ps1 | 52 +++ sync-perizinan.bat | 15 + sync-perizinan.sh | 25 ++ test_all_tables.php | 35 ++ test_dashboard_helper.php | 0 test_terakhir_terbit.php | 31 ++ 35 files changed, 2617 insertions(+), 29 deletions(-) create mode 100644 CHANGES_TERAKHIR_TERBIT.md create mode 100644 FASTEST_INTEGRATION.md create mode 100644 FINAL_OPTIMIZATION_SUMMARY.md create mode 100644 PERIZINAN_API_SETUP.md create mode 100644 PRODUCTION_DEPLOY.md create mode 100644 TASK_SCHEDULER_SETUP.md create mode 100644 app/Console/Commands/CheckPerizinanData.php create mode 100644 app/Console/Commands/SyncPerizinanData.php create mode 100644 app/Console/Commands/TestProductionSetup.php create mode 100644 app/Models/FastestPermohonan.php create mode 100644 app/Models/PerizinanStatus.php create mode 100644 app/Models/TerakhirTerbit.php create mode 100644 app/Services/PerizinanApiService.php create mode 100644 check-environment.sh create mode 100644 check_fastest_data.php create mode 100644 check_terakhir_terbit.php create mode 100644 database/migrations/2025_07_14_015824_create_perizinan_status_table.php create mode 100644 database/migrations/2025_07_14_024445_create_fastest_permohonan_table.php create mode 100644 database/migrations/2025_07_14_033547_create_terakhir_terbit_table.php create mode 100644 debug_api.php create mode 100644 deploy-production.sh create mode 100644 run-scheduler.bat create mode 100644 run-scheduler.sh create mode 100644 setup-task-scheduler.ps1 create mode 100644 sync-perizinan.bat create mode 100644 sync-perizinan.sh create mode 100644 test_all_tables.php create mode 100644 test_dashboard_helper.php create mode 100644 test_terakhir_terbit.php diff --git a/.gitignore b/.gitignore index 4e02a0a..8b0d909 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,11 @@ yarn-error.log /auth.json /.fleet /.idea + +# Production logs (but keep the structure) +/storage/logs/*.log +!/storage/logs/.gitkeep + /.nova /.vscode /.zed diff --git a/CHANGES_TERAKHIR_TERBIT.md b/CHANGES_TERAKHIR_TERBIT.md new file mode 100644 index 0000000..543ac81 --- /dev/null +++ b/CHANGES_TERAKHIR_TERBIT.md @@ -0,0 +1,75 @@ +# PERUBAHAN SISTEM TERAKHIR TERBIT + +## Masalah yang Diperbaiki + +- Menghapus kolom `rank_order` yang statis dan akan bermasalah ketika ada data baru +- Mengubah sistem ordering menggunakan `tanggal_terbit` yang lebih dinamis dan logis + +## Perubahan Yang Dilakukan + +### 1. Database Migration (2025_07_14_033547_create_terakhir_terbit_table.php) + +- ❌ Dihapus: `rank_order` column +- ❌ Dihapus: unique constraint berdasarkan rank_order +- ✅ Ditambah: index pada `kategori` dan `tanggal_terbit` +- ✅ Ditambah: index pada `kategori` dan `sync_date` + +### 2. Model TerakhirTerbit (app/Models/TerakhirTerbit.php) + +- ❌ Dihapus: `rank_order` dari fillable dan casts +- ✅ Diubah: Query method `getLatestByKategori()` menggunakan `ORDER BY tanggal_terbit DESC` + `LIMIT 5` +- ✅ Diubah: Query method `getByKategoriAndDate()` menggunakan `ORDER BY tanggal_terbit DESC` + `LIMIT 5` + +### 3. API Service (app/Services/PerizinanApiService.php) + +- ❌ Dihapus: Logic penambahan `rank_order` dalam `syncTerakhirTerbitToDatabase()` +- ✅ Simplified: Data disimpan tanpa manual ranking, biarkan database yang mengurutkan + +### 4. Dashboard Helper (app/Helpers/DashboardHelper.php) + +- ✅ Diubah: Generate `rank_order` secara dinamis hanya untuk display (tidak disimpan di DB) +- ✅ Data tetap diurutkan berdasarkan `tanggal_terbit` terbaru dari database + +## Keuntungan Sistem Baru + +### ✅ Dinamis + +- Data selalu diurutkan berdasarkan tanggal terbit terbaru +- Tidak ada masalah ketika ada data baru + +### ✅ Consistent + +- Urutan data konsisten berdasarkan logical field (`tanggal_terbit`) +- Tidak bergantung pada urutan API response + +### ✅ Scalable + +- Bisa handle unlimited data tanpa konflik ranking +- Index database optimal untuk performa query + +### ✅ Maintainable + +- Code lebih sederhana tanpa logic manual ranking +- Ranking hanya untuk display purpose + +## Contoh Output Sistem Baru + +``` +PERTEK - Top 5 Terakhir Terbit: +#1: SUMANTO TJAHAJA (02 Jul 2025) ← Terbaru +#2: Eka Ferdiyus Supatmo (16 Jun 2025) ← Kedua terbaru +#3: Adrian Mara Maulana (13 Jun 2025) ← Ketiga terbaru +#4: AHMAD SYARIF (04 Jun 2025) ← Keempat terbaru +#5: drg. WWM Diah Arimbi (28 May 2025) ← Kelima terbaru +``` + +## Migration Command + +```bash +php artisan migrate:fresh # ← Sudah dijalankan +php artisan perizinan:sync --all # ← Test dengan data baru +``` + +## Status: ✅ COMPLETED + +Sistem terakhir terbit sekarang menggunakan ordering dinamis berdasarkan tanggal terbit terbaru. diff --git a/FASTEST_INTEGRATION.md b/FASTEST_INTEGRATION.md new file mode 100644 index 0000000..5a7ee2d --- /dev/null +++ b/FASTEST_INTEGRATION.md @@ -0,0 +1,150 @@ +# Fastest Permohonan API Integration + +## Overview + +Integration telah selesai untuk endpoint fastest permohonan API Jakarta: + +- **URL**: `https://wsdev.jakarta.go.id/gateway/DataPerizinanLingkungan/1.0/fastest_permohonan` +- **Parameters**: `kategori` (pertek/amdal), `limit` (default: 5) +- **Headers**: `Authorization: Bearer `, `x-Gateway-APIKey: ` + +## Database Schema + +### Table: `fastest_permohonan` + +```sql +- id (bigint, primary key) +- kategori (varchar) - pertek, amdal +- nama (varchar) - nama izin dari API +- total (integer) - jumlah total izin +- durasi_pemohon (varchar) - durasi rata-rata pemohon +- durasi_petugas (varchar) - durasi rata-rata petugas +- rank_order (integer) - urutan ranking 1-5 +- api_last_updated (datetime) - dari field API last_updated +- sync_date (date) - tanggal sync data +- created_at, updated_at (timestamps) +``` + +**Indexes:** + +- `(kategori, sync_date, rank_order)` +- Unique: `(kategori, rank_order, sync_date)` + +## API Response Structure + +```json +{ + "success": 1, + "message": "Fastest permohonan retrieved successfully.", + "data": [ + { + "nama": "SURAT PERNYATAAN PENGELOLAAN LINGKUNGAN (SPPL)", + "total": 7174, + "durasi_pemohon": "5 Hari 06 Jam 09 Menit 01 Detik", + "durasi_petugas": "4 Hari 03 Jam 12 Menit 18 Detik" + } + ], + "last_updated": "2025-07-14 09:50:07" +} +``` + +## Files Modified/Created + +### New Files: + +1. `database/migrations/2025_07_14_024445_create_fastest_permohonan_table.php` +2. `app/Models/FastestPermohonan.php` + +### Modified Files: + +1. `app/Services/PerizinanApiService.php` + + - Added `fetchFastestPermohonan()` method + - Added `syncFastestDataToDatabase()` method + - Modified `syncAllCategories()` to include fastest data + +2. `app/Console/Commands/SyncPerizinanData.php` + + - Added `--fastest` option + - Updated to sync both status and fastest data + +3. `routes/console.php` + + - Updated schedule to include `--fastest` flag + +4. `app/Helpers/DashboardHelper.php` + - Added `getFastestPermohonanByType()` method + - Added `getAllFastestData()` method + - Added helper methods for formatting and display + +## Commands + +### Sync Status Data Only: + +```bash +php artisan perizinan:sync # All categories +php artisan perizinan:sync pertek # Specific category +``` + +### Sync Status + Fastest Data: + +```bash +php artisan perizinan:sync --fastest # All categories +php artisan perizinan:sync pertek --fastest # Specific category +``` + +### Check Data: + +```bash +php artisan perizinan:check +``` + +## Scheduled Tasks + +Daily at midnight (00:00 WIB): + +```bash +php artisan perizinan:sync --fastest +``` + +## Data Access Examples + +### Using DashboardHelper: + +```php +// Get fastest data for dashboard +$fastestData = DashboardHelper::getAllFastestData(); + +// Get fastest data for specific type +$pertekFastest = DashboardHelper::getFastestPermohonanByType('pertek'); +``` + +### Using Model Directly: + +```php +// Get today's data for PERTEK +$pertek = FastestPermohonan::getLatestByKategori('pertek'); + +// Get data for specific date +$data = FastestPermohonan::getByKategoriAndDate('amdal', '2025-07-14'); +``` + +## Data Flow + +1. **API Call**: Service fetches data from Jakarta API +2. **Data Validation**: Check API response success +3. **Database Sync**: + - Delete existing data for today (to ensure fresh ranking) + - Insert new data with ranking based on API order +4. **Dashboard Display**: Helper methods format data for frontend + +## Production Deployment + +Data akan tersync otomatis setiap hari jam 12 malam melalui Laravel Scheduler di production server. + +## Notes + +- Data di-refresh setiap hari untuk memastikan ranking terbaru +- Ranking ditentukan berdasarkan urutan dari API response +- Fallback data tersedia jika API tidak tersedia +- Support untuk kategori 'pertek' dan 'amdal' sesuai endpoint yang tersedia diff --git a/FINAL_OPTIMIZATION_SUMMARY.md b/FINAL_OPTIMIZATION_SUMMARY.md new file mode 100644 index 0000000..30cf0e2 --- /dev/null +++ b/FINAL_OPTIMIZATION_SUMMARY.md @@ -0,0 +1,191 @@ +# FINAL SYSTEM IMPROVEMENTS - SEMUA TABEL SUDAH OPTIMAL + +## Ringkasan Perubahan + +✅ **FASTEST_PERMOHONAN** - Dihapus rank_order, gunakan API ordering +✅ **TERAKHIR_TERBIT** - Dihapus rank_order, gunakan tanggal_terbit ordering +✅ **PERIZINAN_STATUS** - Sudah optimal (tidak perlu rank_order) + +## 1. FASTEST_PERMOHONAN Table + +### ❌ Masalah Sebelumnya: + +```php +// MASALAH: rank_order statis, konflik saat ada data baru +'rank_order' => $index + 1 // Static ranking! +``` + +### ✅ Solusi Sekarang: + +```php +// OPTIMAL: Simpan data tanpa ranking, biarkan API order alami +FastestPermohonan::create([ + 'kategori' => $kategori, + 'nama' => $item['nama'], + 'total' => $item['total'], + 'durasi_pemohon' => $item['durasi_pemohon'], + 'durasi_petugas' => $item['durasi_petugas', + // ❌ DIHAPUS: 'rank_order' => $index + 1 + 'api_last_updated' => $apiLastUpdated, + 'sync_date' => $syncDate +]); + +// Query menggunakan insertion order (id) untuk preserve API order +->orderBy('id')->limit(5) +``` + +### 🎯 Keuntungan: + +- **API Order Preserved**: Data tetap sesuai urutan tercepat dari API +- **No Conflicts**: Tidak ada masalah ranking duplicate +- **Dynamic Display**: Ranking (#1-#5) generate saat display + +## 2. TERAKHIR_TERBIT Table + +### ❌ Masalah Sebelumnya: + +```php +// MASALAH: rank_order berdasarkan API response, bukan tanggal terbaru +'rank_order' => $index + 1 // Static dari API! +``` + +### ✅ Solusi Sekarang: + +```php +// OPTIMAL: Urutkan berdasarkan tanggal terbit terbaru +TerakhirTerbit::create([ + 'kategori' => $kategori, + 'nama_izin' => $item['nama_izin'], + 'pemohon' => $item['pemohon'], + 'tanggal_terbit' => Carbon::parse($item['tanggal_terbit']), + // ❌ DIHAPUS: 'rank_order' => $index + 1 + 'api_last_updated' => $apiLastUpdated, + 'sync_date' => $syncDate +]); + +// Query menggunakan logical order (tanggal terbaru) +->orderBy('tanggal_terbit', 'desc')->limit(5) +``` + +### 🎯 Keuntungan: + +- **Always Latest**: Selalu tampilkan izin dengan tanggal terbit terbaru +- **Logical Order**: Urutan berdasarkan field yang relevan (tanggal_terbit) +- **Future Proof**: Data baru otomatis masuk urutan yang benar + +## 3. PERIZINAN_STATUS Table + +### ✅ Sudah Optimal: + +```php +// BAGUS: Cek existing data untuk hari yang sama, no deletion +$exists = PerizinanStatus::where('kategori', $kategori) + ->where('status_id', $item['id']) + ->whereDate('sync_date', $syncDate) + ->exists(); + +if (!$exists) { + // Simpan data baru +} +``` + +### 🎯 Tidak Perlu Perubahan: + +- **Smart Duplication Check**: Hanya cegah data duplicate untuk hari yang sama +- **Historical Data**: Simpan semua data historis untuk analisis +- **No Ranking**: Status data tidak perlu ranking system + +## Database Schema Changes + +### FASTEST_PERMOHONAN Migration: + +```php +// ❌ DIHAPUS: +$table->integer('rank_order'); +$table->unique(['kategori', 'rank_order', 'sync_date']); + +// ✅ DITAMBAH: +$table->index(['kategori', 'total']); // untuk sorting performance +``` + +### TERAKHIR_TERBIT Migration: + +```php +// ❌ DIHAPUS: +$table->integer('rank_order'); +$table->unique(['kategori', 'rank_order', 'sync_date']); + +// ✅ DITAMBAH: +$table->index(['kategori', 'tanggal_terbit']); // untuk sorting performance +``` + +## Model Changes + +### FastestPermohonan Model: + +```php +// ✅ Query Method: +public static function getLatestByKategori($kategori) +{ + return self::where('kategori', $kategori) + ->whereDate('sync_date', Carbon::today()) + ->orderBy('id') // Preserve API order + ->limit(5) + ->get(); +} +``` + +### TerakhirTerbit Model: + +```php +// ✅ Query Method: +public static function getLatestByKategori($kategori) +{ + return self::where('kategori', $kategori) + ->whereDate('sync_date', Carbon::today()) + ->orderBy('tanggal_terbit', 'desc') // Latest first + ->limit(5) + ->get(); +} +``` + +## Dashboard Helper Changes + +### Dynamic Ranking Generation: + +```php +// ✅ Generate ranking hanya untuk display +$index = 1; +foreach ($dbData as $item) { + $data[] = [ + 'nama' => $item->nama, + // ...data lainnya... + 'rank_order' => $index++ // Dynamic display ranking + ]; +} +``` + +## Hasil Test Final + +``` +FASTEST DATA (API ORDER): +#1: SPPL UNTUK KEPENTINGAN BANGUNAN MILIK PEMERINTAH (6 total, 3j54m) ← Tercepat +#2: SPPL (7,174 total, 5h6j9m) ← Kedua tercepat +#3: Izin Tempat Uji Emisi (592 total, 5h1j57m) ← Ketiga tercepat + +TERAKHIR TERBIT (TANGGAL TERBARU): +#1: SUMANTO TJAHAJA (02 Jul 2025) ← Terbaru +#2: Eka Ferdiyus Supatmo (16 Jun 2025) ← Kedua terbaru +#3: Adrian Mara Maulana (13 Jun 2025) ← Ketiga terbaru +``` + +## ✅ STATUS: SYSTEM FULLY OPTIMIZED + +Semua tabel sekarang menggunakan: + +- **NO Static rank_order** yang bermasalah +- **Logical ordering** berdasarkan data relevance +- **Dynamic ranking** untuk display purposes +- **Scalable architecture** untuk data masa depan + +🎉 **System ready for production!** diff --git a/PERIZINAN_API_SETUP.md b/PERIZINAN_API_SETUP.md new file mode 100644 index 0000000..6cdfd69 --- /dev/null +++ b/PERIZINAN_API_SETUP.md @@ -0,0 +1,164 @@ +# Setup Perizinan API Integration + +## Overview + +Sistem ini mengintegrasikan data perizinan dari API Jakarta dengan database lokal menggunakan cron job yang berjalan setiap hari pada jam 12 malam. + +## Setup Instructions + +### 1. Environment Configuration + +Update file `.env` dengan credentials API: + +```env +# Perizinan API Configuration +PERIZINAN_API_BASE_URL=https://wsdev.jakarta.go.id/gateway/DataPerizinanLingkungan/1.0 +PERIZINAN_API_BEARER_TOKEN=your_actual_bearer_token +PERIZINAN_API_KEY=your_actual_api_key +``` + +### 2. Database Migration + +Jalankan migration untuk membuat tabel: + +```bash +php artisan migrate +``` + +### 3. Manual Sync (Testing) + +Untuk test sync manual: + +```bash +# Sync semua kategori +php artisan perizinan:sync + +# Sync kategori tertentu +php artisan perizinan:sync pertek +php artisan perizinan:sync amdal +``` + +### 4. Cron Job Setup (Linux Production Server) + +Sistem sudah dikonfigurasi untuk berjalan otomatis setiap hari jam 12 malam. Untuk aktivasi cron job di server Linux: + +#### Opsi 1: Laravel Scheduler (Recommended) + +```bash +# Edit crontab +crontab -e + +# Add this line (runs every minute): +* * * * * cd /var/www/perling && ./run-scheduler.sh >/dev/null 2>&1 +``` + +#### Opsi 2: Direct Sync + +```bash +# Edit crontab +crontab -e + +# Add this line (runs daily at midnight): +0 0 * * * cd /var/www/perling && ./sync-perizinan.sh >/dev/null 2>&1 +``` + +### 5. Production Deployment + +Untuk deployment ke production server, lihat file `PRODUCTION_DEPLOY.md` untuk panduan lengkap. + +### 6. Monitoring + +Cek log file untuk monitoring sync process: + +```bash +# Application logs +tail -f storage/logs/laravel.log + +# Cron job logs +tail -f storage/logs/cron.log + +# Scheduler logs (if using Laravel Scheduler) +tail -f storage/logs/scheduler.log + +# Check cron service status +sudo systemctl status cron + +# View cron job list +crontab -l +``` + +## API Response Format + +```json +{ + "success": true, + "message": "Summary by status retrieved successfully.", + "data": [ + { + "id": "ditolak", + "label": "Izin Ditolak", + "value": 1443 + }, + { + "id": "selesai", + "label": "Izin Selesai", + "value": 8379 + }, + { + "id": "proses", + "label": "Dalam Proses", + "value": 143 + }, + { + "id": "total", + "label": "Total Pengajuan", + "value": 9965 + } + ], + "last_updated": "2025-07-14 08:55:04" +} +``` + +## Database Schema + +Table: `perizinan_status` + +- `id` - Primary key +- `kategori` - Category (pertek, amdal) +- `status_id` - Status ID (ditolak, selesai, proses, total) +- `label` - Human readable label +- `value` - Count value +- `api_last_updated` - Last updated timestamp from API +- `sync_date` - Date when data was synced +- `created_at` / `updated_at` - Laravel timestamps + +## Features + +- ✅ Daily automatic sync at midnight +- ✅ Duplicate prevention (same data per day) +- ✅ Comprehensive logging +- ✅ Fallback to static data if API fails +- ✅ Manual sync commands for testing +- ✅ Database storage with indexing for performance + +## Troubleshooting + +### Command not found + +```bash +php artisan optimize:clear +composer dump-autoload +``` + +### API Connection Issues + +1. Verify credentials in `.env` +2. Check network connectivity +3. Review logs for detailed error messages + +### Database Issues + +```bash +php artisan migrate:fresh +php artisan migrate +``` diff --git a/PRODUCTION_DEPLOY.md b/PRODUCTION_DEPLOY.md new file mode 100644 index 0000000..d71bec5 --- /dev/null +++ b/PRODUCTION_DEPLOY.md @@ -0,0 +1,191 @@ +# Production Deployment Guide - Perizinan API Integration + +## 🚀 Quick Deployment + +### 1. Upload Files to Server + +Upload semua files ke server Linux di directory `/var/www/perling` (atau sesuai setup Anda). + +### 2. Run Deployment Script + +```bash +cd /var/www/perling +chmod +x deploy-production.sh +./deploy-production.sh +``` + +### 3. Update Environment Variables + +Edit file `.env` dan update: + +```env +APP_ENV=production +APP_DEBUG=false + +# Database production +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=perling_production +DB_USERNAME=your_db_user +DB_PASSWORD=your_db_password + +# API Credentials (yang sudah ada) +PERIZINAN_API_BEARER_TOKEN=f633c4f8e8a84708895bf31c0afdfa68c9079613d18d4edfb1fc6a0019b5f832 +PERIZINAN_API_KEY=b06c8ef1-06db-443d-996b-80e8c3374923 +``` + +### 4. Setup Cron Job + +#### Option 1: Laravel Scheduler (Recommended) + +```bash +# Edit crontab +sudo crontab -e + +# Add this line (runs every minute): +* * * * * cd /var/www/perling && ./run-scheduler.sh >/dev/null 2>&1 +``` + +#### Option 2: Direct Sync + +```bash +# Edit crontab +sudo crontab -e + +# Add this line (runs daily at midnight): +0 0 * * * cd /var/www/perling && ./sync-perizinan.sh >/dev/null 2>&1 +``` + +## 🔧 Manual Commands + +### Test API Connection + +```bash +cd /var/www/perling +php artisan perizinan:sync pertek +``` + +### Check Synced Data + +```bash +php artisan perizinan:check +``` + +### View Logs + +```bash +# Cron logs +tail -f storage/logs/cron.log + +# Laravel application logs +tail -f storage/logs/laravel.log + +# Scheduler logs (if using Option 1) +tail -f storage/logs/scheduler.log +``` + +### View Cron Job Status + +```bash +# View current crontab +crontab -l + +# View cron service logs +sudo journalctl -u cron -f +``` + +## 🐛 Troubleshooting + +### Permission Issues + +```bash +# Fix storage permissions +sudo chown -R www-data:www-data storage/ +sudo chmod -R 775 storage/ + +# Fix cache permissions +sudo chown -R www-data:www-data bootstrap/cache/ +sudo chmod -R 775 bootstrap/cache/ +``` + +### Database Issues + +```bash +# Clear config cache +php artisan config:clear + +# Run migrations +php artisan migrate --force + +# Check database connection +php artisan tinker --execute="DB::connection()->getPdo();" +``` + +### API Connection Issues + +```bash +# Test API manually +curl -X GET "https://wsdev.jakarta.go.id/gateway/DataPerizinanLingkungan/1.0/summary_by_status?kategori=pertek" \ + -H "Authorization: Bearer f633c4f8e8a84708895bf31c0afdfa68c9079613d18d4edfb1fc6a0019b5f832" \ + -H "x-Gateway-APIKey: b06c8ef1-06db-443d-996b-80e8c3374923" +``` + +### Cron Job Not Running + +```bash +# Check if cron service is running +sudo systemctl status cron + +# Start cron service if stopped +sudo systemctl start cron + +# Check cron logs +sudo tail -f /var/log/cron.log +``` + +## 📊 Monitoring + +### Daily Health Check + +```bash +# Check if data was synced today +php artisan perizinan:check + +# Check latest logs +tail -20 storage/logs/cron.log +``` + +### Weekly Review + +```bash +# Check database records count +php artisan tinker --execute="echo App\Models\PerizinanStatus::count() . ' total records';" + +# Check logs for errors +grep -i error storage/logs/laravel.log | tail -10 +``` + +## 🔐 Security Notes + +1. **File Permissions**: Ensure proper permissions for Laravel +2. **Database**: Use strong password and limit access +3. **API Keys**: Keep credentials secure, don't commit to version control +4. **Logs**: Regularly rotate and clean up log files +5. **SSL**: Use HTTPS in production + +## 📈 Performance Tips + +1. **Database Indexing**: Already added in migration +2. **Log Rotation**: Setup logrotate for Laravel logs +3. **Caching**: Enable OPcache for PHP +4. **Queue**: Consider using Redis for Laravel queues if needed + +## 🎯 Success Criteria + +✅ Cron job runs without errors +✅ Data syncs daily at midnight +✅ Dashboard shows real API data +✅ Logs are clean without errors +✅ Database grows with daily records +✅ API connection is stable diff --git a/TASK_SCHEDULER_SETUP.md b/TASK_SCHEDULER_SETUP.md new file mode 100644 index 0000000..0dd1197 --- /dev/null +++ b/TASK_SCHEDULER_SETUP.md @@ -0,0 +1,73 @@ +# Manual Task Scheduler Setup Instructions + +## Cara Setup Task Scheduler Manual di Windows + +### Langkah 1: Buka Task Scheduler + +1. Tekan `Win + R` +2. Ketik `taskschd.msc` dan tekan Enter +3. Atau search "Task Scheduler" di Start Menu + +### Langkah 2: Create Basic Task + +1. Di panel kanan, klik **"Create Basic Task..."** +2. **Name**: `PerizinanDataSync` +3. **Description**: `Daily sync of perizinan data from Jakarta API at midnight` +4. Klik **Next** + +### Langkah 3: Trigger (Kapan berjalan) + +1. Pilih **"Daily"** +2. Klik **Next** +3. **Start date**: Hari ini +4. **Start time**: `12:00:00 AM` (midnight) +5. **Recur every**: `1 days` +6. Klik **Next** + +### Langkah 4: Action (Apa yang dijalankan) + +1. Pilih **"Start a program"** +2. Klik **Next** +3. **Program/script**: `C:\laragon\www\perling\sync-perizinan.bat` +4. **Start in**: `C:\laragon\www\perling` +5. Klik **Next** + +### Langkah 5: Finish + +1. Review semua setting +2. ✅ Centang **"Open the Properties dialog for this task when I click Finish"** +3. Klik **Finish** + +### Langkah 6: Advanced Settings (Optional) + +Di Properties dialog: + +1. Tab **General**: + + - ✅ Centang "Run whether user is logged on or not" + - ✅ Centang "Run with highest privileges" + +2. Tab **Conditions**: + + - ❌ Uncheck "Start the task only if the computer is on AC power" + +3. Tab **Settings**: + + - ✅ Centang "Allow task to be run on demand" + - ✅ Centang "Run task as soon as possible after a scheduled start is missed" + +4. Klik **OK** + +## Test Task + +Untuk test task manual: + +1. Klik kanan pada task **"PerizinanDataSync"** +2. Pilih **"Run"** +3. Check log di: `C:\laragon\www\perling\storage\logs\cron.log` + +## Monitoring + +- Task history bisa dilihat di tab **History** pada task properties +- Log aplikasi di: `storage\logs\cron.log` +- Log Laravel di: `storage\logs\laravel.log` diff --git a/app/Console/Commands/CheckPerizinanData.php b/app/Console/Commands/CheckPerizinanData.php new file mode 100644 index 0000000..3e90039 --- /dev/null +++ b/app/Console/Commands/CheckPerizinanData.php @@ -0,0 +1,38 @@ +info('📊 Checking synced perizinan data...'); + + $data = PerizinanStatus::orderBy('kategori')->orderBy('status_id')->get(); + + if ($data->isEmpty()) { + $this->warn('No data found in database'); + return; + } + + $groupedData = $data->groupBy('kategori'); + + foreach ($groupedData as $kategori => $items) { + $this->newLine(); + $this->info("=== {$kategori} ==="); + + foreach ($items as $item) { + $this->line(" {$item->status_id}: {$item->label} = {$item->value}"); + } + } + + $this->newLine(); + $this->info("Total records: " . $data->count()); + } +} diff --git a/app/Console/Commands/SyncPerizinanData.php b/app/Console/Commands/SyncPerizinanData.php new file mode 100644 index 0000000..f548d29 --- /dev/null +++ b/app/Console/Commands/SyncPerizinanData.php @@ -0,0 +1,154 @@ +argument('kategori'); + $includeFastest = $this->option('fastest'); + $includeTerakhirTerbit = $this->option('terakhir-terbit'); + $includeAll = $this->option('all'); + + // If --all is specified, include both fastest and terakhir terbit + if ($includeAll) { + $includeFastest = true; + $includeTerakhirTerbit = true; + } + + Log::info('Starting perizinan data sync', [ + 'kategori' => $kategori, + 'include_fastest' => $includeFastest, + 'include_terakhir_terbit' => $includeTerakhirTerbit, + 'start_time' => $startTime->format('Y-m-d H:i:s') + ]); + + $this->info('🚀 Starting perizinan data synchronization...'); + if ($includeFastest) { + $this->info('⚡ Including fastest permohonan data sync'); + } + if ($includeTerakhirTerbit) { + $this->info('📋 Including terakhir terbit data sync'); + } + + $service = new PerizinanApiService(); + + try { + if ($kategori) { + // Sync specific category + $this->info("📥 Syncing status data for kategori: {$kategori}"); + $statusResult = $service->syncToDatabase($kategori); + + if ($statusResult) { + $this->info("✅ Successfully synced status data for kategori: {$kategori}"); + } else { + $this->error("❌ Failed to sync status data for kategori: {$kategori}"); + } + + // Sync fastest data if requested + if ($includeFastest) { + $this->info("⚡ Syncing fastest permohonan data for kategori: {$kategori}"); + $fastestResult = $service->syncFastestDataToDatabase($kategori, 5); + + if ($fastestResult) { + $this->info("✅ Successfully synced fastest data for kategori: {$kategori}"); + } else { + $this->error("❌ Failed to sync fastest data for kategori: {$kategori}"); + } + } + + // Sync terakhir terbit data if requested + if ($includeTerakhirTerbit) { + $this->info("📋 Syncing terakhir terbit data for kategori: {$kategori}"); + $terakhirTerbitResult = $service->syncTerakhirTerbitToDatabase($kategori, 5); + + if ($terakhirTerbitResult) { + $this->info("✅ Successfully synced terakhir terbit data for kategori: {$kategori}"); + } else { + $this->error("❌ Failed to sync terakhir terbit data for kategori: {$kategori}"); + } + } + } else { + // Sync all categories + $this->info("📥 Syncing data for all categories..."); + $results = $service->syncAllCategories($includeFastest, $includeTerakhirTerbit); + + foreach ($results as $cat => $result) { + if (isset($result['status']) && $result['status']) { + $this->info("✅ Successfully synced status data for kategori: {$cat}"); + } else { + $this->error("❌ Failed to sync status data for kategori: {$cat}"); + } + + if ($includeFastest) { + if (isset($result['fastest']) && $result['fastest']) { + $this->info("✅ Successfully synced fastest data for kategori: {$cat}"); + } else { + $this->error("❌ Failed to sync fastest data for kategori: {$cat}"); + } + } + + if ($includeTerakhirTerbit) { + if (isset($result['terakhir_terbit']) && $result['terakhir_terbit']) { + $this->info("✅ Successfully synced terakhir terbit data for kategori: {$cat}"); + } else { + $this->error("❌ Failed to sync terakhir terbit data for kategori: {$cat}"); + } + } + } + } + + $endTime = Carbon::now(); + $duration = $endTime->diffInSeconds($startTime); + + $this->info("🎉 Data synchronization completed in {$duration} seconds"); + + Log::info('Perizinan data sync completed', [ + 'kategori' => $kategori ?: 'all', + 'include_fastest' => $includeFastest, + 'include_terakhir_terbit' => $includeTerakhirTerbit, + 'duration_seconds' => $duration, + 'end_time' => $endTime->format('Y-m-d H:i:s') + ]); + + } catch (\Exception $e) { + $this->error("💥 An error occurred: " . $e->getMessage()); + + Log::error('Perizinan data sync failed', [ + 'kategori' => $kategori ?: 'all', + 'include_fastest' => $includeFastest, + 'include_terakhir_terbit' => $includeTerakhirTerbit, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return 1; + } + + return 0; + } +} diff --git a/app/Console/Commands/TestProductionSetup.php b/app/Console/Commands/TestProductionSetup.php new file mode 100644 index 0000000..bd865c6 --- /dev/null +++ b/app/Console/Commands/TestProductionSetup.php @@ -0,0 +1,122 @@ +info('🔍 Testing Production Setup Readiness...'); + $this->newLine(); + + $allPassed = true; + + // Test 1: Database Connection + $this->info('1. Testing Database Connection...'); + try { + DB::connection()->getPdo(); + $this->line(' ✅ Database connection successful'); + } catch (\Exception $e) { + $this->error(' ❌ Database connection failed: ' . $e->getMessage()); + $allPassed = false; + } + + // Test 2: Database Table + $this->info('2. Testing Database Table...'); + try { + $count = PerizinanStatus::count(); + $this->line(" ✅ perizinan_status table exists with {$count} records"); + } catch (\Exception $e) { + $this->error(' ❌ Database table issue: ' . $e->getMessage()); + $allPassed = false; + } + + // Test 3: API Configuration + $this->info('3. Testing API Configuration...'); + $bearerToken = config('services.perizinan.bearer_token'); + $apiKey = config('services.perizinan.api_key'); + + if ($bearerToken && $bearerToken !== 'your_actual_bearer_token') { + $this->line(' ✅ Bearer token configured'); + } else { + $this->error(' ❌ Bearer token not configured properly'); + $allPassed = false; + } + + if ($apiKey && $apiKey !== 'your_actual_api_key') { + $this->line(' ✅ API key configured'); + } else { + $this->error(' ❌ API key not configured properly'); + $allPassed = false; + } + + // Test 4: API Connection + $this->info('4. Testing API Connection...'); + try { + $service = new PerizinanApiService(); + $response = $service->fetchSummaryByStatus('pertek'); + + if ($response && ($response['success'] ?? false)) { + $this->line(' ✅ API connection successful'); + $dataCount = count($response['data'] ?? []); + $this->line(" 📊 Retrieved {$dataCount} data items"); + } else { + $this->error(' ❌ API response invalid'); + $allPassed = false; + } + } catch (\Exception $e) { + $this->error(' ❌ API connection failed: ' . $e->getMessage()); + $allPassed = false; + } + + // Test 5: Storage Permissions + $this->info('5. Testing Storage Permissions...'); + $logPath = storage_path('logs/test-' . time() . '.log'); + try { + file_put_contents($logPath, 'test'); + unlink($logPath); + $this->line(' ✅ Storage directory writable'); + } catch (\Exception $e) { + $this->error(' ❌ Storage permission issue: ' . $e->getMessage()); + $allPassed = false; + } + + // Test 6: Schedule Configuration + $this->info('6. Testing Schedule Configuration...'); + try { + Artisan::call('schedule:list'); + $this->line(' ✅ Laravel scheduler configured'); + $this->line(' 📅 Run "php artisan schedule:list" to see scheduled commands'); + } catch (\Exception $e) { + $this->error(' ❌ Scheduler issue: ' . $e->getMessage()); + $allPassed = false; + } + + $this->newLine(); + + if ($allPassed) { + $this->info('🎉 All tests passed! Production setup is ready.'); + $this->newLine(); + $this->info('Next steps:'); + $this->line('1. Deploy to production server'); + $this->line('2. Run: chmod +x *.sh'); + $this->line('3. Run: ./deploy-production.sh'); + $this->line('4. Setup cron job as per PRODUCTION_DEPLOY.md'); + } else { + $this->error('❌ Some tests failed. Please fix the issues above.'); + return 1; + } + + return 0; + } +} diff --git a/app/Helpers/DashboardHelper.php b/app/Helpers/DashboardHelper.php index 098f79a..148d630 100644 --- a/app/Helpers/DashboardHelper.php +++ b/app/Helpers/DashboardHelper.php @@ -2,12 +2,17 @@ namespace App\Helpers; +use App\Models\PerizinanStatus; +use App\Models\FastestPermohonan; +use App\Models\TerakhirTerbit; +use Carbon\Carbon; + class DashboardHelper { /** - * Data master untuk setiap jenis izin + * Data master untuk setiap jenis izin - fallback untuk data yang belum ada di API */ - public static function getStatusDataByType(string $type): array + public static function getFallbackDataByType(string $type): array { $data = [ 'pertek' => [ @@ -15,16 +20,16 @@ class DashboardHelper ['id' => 'selesai', 'label' => 'Izin Selesai', 'value' => 45, 'color' => 'success'], ['id' => 'proses', 'label' => 'Dalam Proses', 'value' => 12, 'color' => 'info'], ], - 'rintek' => [ - ['id' => 'ditolak', 'label' => 'Izin Ditolak', 'value' => 5, 'color' => 'danger'], - ['id' => 'selesai', 'label' => 'Izin Selesai', 'value' => 28, 'color' => 'success'], - ['id' => 'proses', 'label' => 'Dalam Proses', 'value' => 7, 'color' => 'info'], - ], 'amdal' => [ ['id' => 'ditolak', 'label' => 'Izin Ditolak', 'value' => 3, 'color' => 'danger'], ['id' => 'selesai', 'label' => 'Izin Selesai', 'value' => 15, 'color' => 'success'], ['id' => 'proses', 'label' => 'Dalam Proses', 'value' => 9, 'color' => 'info'], ], + 'rintek' => [ + ['id' => 'ditolak', 'label' => 'Izin Ditolak', 'value' => 5, 'color' => 'danger'], + ['id' => 'selesai', 'label' => 'Izin Selesai', 'value' => 28, 'color' => 'success'], + ['id' => 'proses', 'label' => 'Dalam Proses', 'value' => 7, 'color' => 'info'], + ], 'izin_angkut' => [ ['id' => 'ditolak', 'label' => 'Izin Ditolak', 'value' => 12, 'color' => 'danger'], ['id' => 'selesai', 'label' => 'Izin Selesai', 'value' => 67, 'color' => 'success'], @@ -40,18 +45,56 @@ class DashboardHelper return $data[$type] ?? []; } + /** + * Get status data by type from database or fallback + */ + public static function getStatusDataByType(string $type): array + { + // Cek apakah ada data di database untuk hari ini + $dbData = PerizinanStatus::getLatestByKategori($type); + + if ($dbData->isNotEmpty()) { + $data = []; + $colorMap = [ + 'ditolak' => 'danger', + 'selesai' => 'success', + 'proses' => 'info', + 'total' => 'primary' + ]; + + foreach ($dbData as $item) { + $data[] = [ + 'id' => $item->status_id, + 'label' => $item->label, + 'value' => $item->value, + 'color' => $colorMap[$item->status_id] ?? 'secondary' + ]; + } + + return $data; + } + + // Fallback ke data statis jika tidak ada data di database + return self::getFallbackDataByType($type); + } + /** * Menambahkan total ke data statuses */ public static function addTotalToStatuses(array $statuses): array { - $total = array_sum(array_column($statuses, 'value')); - $statuses[] = [ - 'id' => 'total', - 'label' => 'Total Pengajuan', - 'value' => $total, - 'color' => 'primary' - ]; + // Cek apakah sudah ada total di data + $hasTotal = collect($statuses)->contains('id', 'total'); + + if (!$hasTotal) { + $total = array_sum(array_column($statuses, 'value')); + $statuses[] = [ + 'id' => 'total', + 'label' => 'Total Pengajuan', + 'value' => $total, + 'color' => 'primary' + ]; + } return $statuses; } @@ -224,4 +267,181 @@ class DashboardHelper return $colors[$statusId] ?? '#6b7280'; } + + /** + * Get fastest permohonan data from database or fallback + */ + public static function getFastestPermohonanByType(string $type): array + { + // Try to get data from database first + $dbData = FastestPermohonan::getLatestByKategori($type); + + if ($dbData->isNotEmpty()) { + $data = []; + $index = 1; + foreach ($dbData as $item) { + $data[] = [ + 'nama' => $item->nama, + 'total' => $item->total, + 'durasi_pemohon' => $item->durasi_pemohon, + 'durasi_petugas' => $item->durasi_petugas, + 'rank_order' => $index++ // Generate rank_order dinamis untuk display + ]; + } + return $data; + } + + // Fallback to static data if no database data available + return self::getFallbackFastestData($type); + } + + /** + * Fallback static data for fastest permohonan + */ + private static function getFallbackFastestData(string $type): array + { + $fallbackData = [ + 'pertek' => [ + ['nama' => 'Izin Pengelolaan Limbah B3', 'total' => 45, 'durasi_pemohon' => '2 Hari 3 Jam', 'durasi_petugas' => '5 Hari 2 Jam', 'rank_order' => 1], + ['nama' => 'Izin Emisi Kendaraan', 'total' => 67, 'durasi_pemohon' => '3 Hari 1 Jam', 'durasi_petugas' => '7 Hari 4 Jam', 'rank_order' => 2], + ['nama' => 'SPPL Industri', 'total' => 23, 'durasi_pemohon' => '4 Hari 2 Jam', 'durasi_petugas' => '8 Hari 1 Jam', 'rank_order' => 3], + ['nama' => 'Izin Penyimpanan B3', 'total' => 34, 'durasi_pemohon' => '5 Hari', 'durasi_petugas' => '9 Hari 3 Jam', 'rank_order' => 4], + ['nama' => 'Izin Pengolahan Limbah', 'total' => 19, 'durasi_pemohon' => '6 Hari 4 Jam', 'durasi_petugas' => '10 Hari 2 Jam', 'rank_order' => 5], + ], + 'amdal' => [ + ['nama' => 'AMDAL Bangunan Tinggi', 'total' => 12, 'durasi_pemohon' => '15 Hari', 'durasi_petugas' => '30 Hari', 'rank_order' => 1], + ['nama' => 'AMDAL Infrastruktur', 'total' => 8, 'durasi_pemohon' => '18 Hari', 'durasi_petugas' => '35 Hari', 'rank_order' => 2], + ['nama' => 'AMDAL Industri', 'total' => 15, 'durasi_pemohon' => '20 Hari', 'durasi_petugas' => '40 Hari', 'rank_order' => 3], + ['nama' => 'AMDAL Perumahan', 'total' => 25, 'durasi_pemohon' => '22 Hari', 'durasi_petugas' => '42 Hari', 'rank_order' => 4], + ['nama' => 'AMDAL Komersial', 'total' => 18, 'durasi_pemohon' => '25 Hari', 'durasi_petugas' => '45 Hari', 'rank_order' => 5], + ] + ]; + + return $fallbackData[$type] ?? []; + } + + /** + * Get fastest data for all supported types + */ + public static function getAllFastestData(): array + { + $supportedTypes = ['pertek', 'amdal']; // Only types with API endpoints + $allData = []; + + foreach ($supportedTypes as $type) { + $allData[$type] = [ + 'type' => $type, + 'label' => self::getTypeLabel($type), + 'data' => self::getFastestPermohonanByType($type) + ]; + } + + return $allData; + } + + /** + * Format duration text for display + */ + public static function formatDuration(string $duration): string + { + // API returns format like "03 Jam 54 Menit 13 Detik" or "2 Hari 01 Jam 02 Menit 04 Detik" + return $duration; + } + + /** + * Get rank badge color based on ranking + */ + public static function getRankBadgeColor(int $rank): string + { + $colors = [ + 1 => 'success', + 2 => 'info', + 3 => 'warning', + 4 => 'secondary', + 5 => 'dark' + ]; + + return $colors[$rank] ?? 'secondary'; + } + + /** + * Get terakhir terbit data from database or fallback + */ + public static function getTerakhirTerbitByType(string $type): array + { + // Try to get data from database first + $dbData = TerakhirTerbit::getLatestByKategori($type); + + if ($dbData->isNotEmpty()) { + $data = []; + $index = 1; + foreach ($dbData as $item) { + $data[] = [ + 'nama_izin' => $item->nama_izin, + 'pemohon' => $item->pemohon, + 'tanggal_terbit' => $item->tanggal_terbit, + 'rank_order' => $index++ // Generate rank_order dinamis untuk display + ]; + } + return $data; + } + + // Fallback to static data if no database data available + return self::getFallbackTerakhirTerbitData($type); + } + + /** + * Fallback static data for terakhir terbit + */ + private static function getFallbackTerakhirTerbitData(string $type): array + { + $fallbackData = [ + 'pertek' => [ + ['nama_izin' => 'SERTIFIKAT LAIK OPERASI - PEMENUHAN BAKU MUTU EMISI', 'pemohon' => 'PT. MAJU BERSAMA', 'tanggal_terbit' => Carbon::parse('2025-07-15'), 'rank_order' => 1], + ['nama_izin' => 'PERSETUJUAN TEKNIS - PEMENUHAN BAKU MUTU AIR LIMBAH', 'pemohon' => 'CV. KARYA MANDIRI', 'tanggal_terbit' => Carbon::parse('2025-07-14'), 'rank_order' => 2], + ['nama_izin' => 'SERTIFIKAT LAIK OPERASI - PEMENUHAN BAKU MUTU AIR LIMBAH', 'pemohon' => 'PT. INDUSTRI SEJAHTERA', 'tanggal_terbit' => Carbon::parse('2025-07-13'), 'rank_order' => 3], + ['nama_izin' => 'PERSETUJUAN TEKNIS - PEMENUHAN BAKU MUTU AIR LIMBAH', 'pemohon' => 'PT. TEKNOLOGI MODERN', 'tanggal_terbit' => Carbon::parse('2025-07-12'), 'rank_order' => 4], + ['nama_izin' => 'SERTIFIKAT LAIK OPERASI - PEMENUHAN BAKU MUTU EMISI', 'pemohon' => 'CV. BERKAH JAYA', 'tanggal_terbit' => Carbon::parse('2025-07-11'), 'rank_order' => 5], + ], + 'amdal' => [ + ['nama_izin' => 'AMDAL BANGUNAN TINGGI', 'pemohon' => 'PT. KONSTRUKSI PRIMA', 'tanggal_terbit' => Carbon::parse('2025-07-10'), 'rank_order' => 1], + ['nama_izin' => 'AMDAL INFRASTRUKTUR', 'pemohon' => 'CV. INFRATEK', 'tanggal_terbit' => Carbon::parse('2025-07-09'), 'rank_order' => 2], + ['nama_izin' => 'AMDAL INDUSTRI', 'pemohon' => 'PT. INDUSTRI MODERN', 'tanggal_terbit' => Carbon::parse('2025-07-08'), 'rank_order' => 3], + ['nama_izin' => 'AMDAL PERUMAHAN', 'pemohon' => 'PT. PROPERTI SEJAHTERA', 'tanggal_terbit' => Carbon::parse('2025-07-07'), 'rank_order' => 4], + ['nama_izin' => 'AMDAL KOMERSIAL', 'pemohon' => 'CV. KOMERSIAL JAYA', 'tanggal_terbit' => Carbon::parse('2025-07-06'), 'rank_order' => 5], + ] + ]; + + return $fallbackData[$type] ?? []; + } + + /** + * Get terakhir terbit data for all supported types + */ + public static function getAllTerakhirTerbitData(): array + { + $supportedTypes = ['pertek', 'amdal']; // Only types with API endpoints + $allData = []; + + foreach ($supportedTypes as $type) { + $allData[$type] = [ + 'type' => $type, + 'label' => self::getTypeLabel($type), + 'data' => self::getTerakhirTerbitByType($type) + ]; + } + + return $allData; + } + + /** + * Format tanggal terbit for display + */ + public static function formatTanggalTerbit($tanggal): string + { + if ($tanggal instanceof Carbon) { + return $tanggal->format('d M Y'); + } + return Carbon::parse($tanggal)->format('d M Y'); + } } diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 1badeec..52a22a6 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -8,14 +8,23 @@ use Illuminate\Support\Facades\Log; use App\Helpers\DashboardHelper; class DashboardController extends Controller -{ - public function index() +{ public function index() { + // Get all statistics for all types + $allStatistics = DashboardHelper::getAllStatistics(); + + // Get fastest data for types that have API endpoints + $allFastestData = DashboardHelper::getAllFastestData(); + + // Get terakhir terbit data for types that have API endpoints + $allTerakhirTerbitData = DashboardHelper::getAllTerakhirTerbitData(); + + // Default data for main view (PERTEK) $statuses = DashboardHelper::getStatusDataByType('pertek'); $statuses = DashboardHelper::addTotalToStatuses($statuses); $type = 'pertek'; - return view('dashboard/index', compact('statuses', 'type')); + return view('dashboard/index', compact('statuses', 'type', 'allStatistics', 'allFastestData', 'allTerakhirTerbitData')); } public function pertek() diff --git a/app/Models/FastestPermohonan.php b/app/Models/FastestPermohonan.php new file mode 100644 index 0000000..f886921 --- /dev/null +++ b/app/Models/FastestPermohonan.php @@ -0,0 +1,54 @@ + 'datetime', + 'sync_date' => 'date', + 'total' => 'integer' + ]; + + /** + * Get fastest permohonan by kategori for today, ordered by API response order + * Since API already provides fastest data in correct order + */ + public static function getLatestByKategori($kategori) + { + return self::where('kategori', $kategori) + ->whereDate('sync_date', Carbon::today()) + ->orderBy('id') // Keep API order by using insertion order + ->limit(5) + ->get(); + } + + /** + * Get data for specific kategori and date + */ + public static function getByKategoriAndDate($kategori, $date = null) + { + $date = $date ?: Carbon::today(); + + return self::where('kategori', $kategori) + ->whereDate('sync_date', $date) + ->orderBy('id') // Keep API order by using insertion order + ->limit(5) + ->get(); + } +} diff --git a/app/Models/PerizinanStatus.php b/app/Models/PerizinanStatus.php new file mode 100644 index 0000000..301783b --- /dev/null +++ b/app/Models/PerizinanStatus.php @@ -0,0 +1,50 @@ + 'datetime', + 'sync_date' => 'date', + 'value' => 'integer' + ]; + + /** + * Get latest data by kategori + */ + public static function getLatestByKategori($kategori) + { + return self::where('kategori', $kategori) + ->whereDate('sync_date', Carbon::today()) + ->get() + ->keyBy('status_id'); + } + + /** + * Get data for specific kategori and date + */ + public static function getByKategoriAndDate($kategori, $date = null) + { + $date = $date ?: Carbon::today(); + + return self::where('kategori', $kategori) + ->whereDate('sync_date', $date) + ->get() + ->keyBy('status_id'); + } +} diff --git a/app/Models/TerakhirTerbit.php b/app/Models/TerakhirTerbit.php new file mode 100644 index 0000000..689cee9 --- /dev/null +++ b/app/Models/TerakhirTerbit.php @@ -0,0 +1,50 @@ + 'date', + 'api_last_updated' => 'datetime', + 'sync_date' => 'date' + ]; + + /** + * Get latest data by kategori for today, ordered by tanggal_terbit DESC + */ + public static function getLatestByKategori($kategori) + { + return self::where('kategori', $kategori) + ->whereDate('sync_date', Carbon::today()) + ->orderBy('tanggal_terbit', 'desc') + ->limit(5) + ->get(); + } + + /** + * Get data by kategori and specific date, ordered by tanggal_terbit DESC + */ + public static function getByKategoriAndDate($kategori, $date) + { + return self::where('kategori', $kategori) + ->whereDate('sync_date', $date) + ->orderBy('tanggal_terbit', 'desc') + ->limit(5) + ->get(); + } +} diff --git a/app/Services/PerizinanApiService.php b/app/Services/PerizinanApiService.php new file mode 100644 index 0000000..1a2a89f --- /dev/null +++ b/app/Services/PerizinanApiService.php @@ -0,0 +1,358 @@ +baseUrl = config('services.perizinan.base_url', 'https://wsdev.jakarta.go.id/gateway/DataPerizinanLingkungan/1.0'); + $this->bearerToken = config('services.perizinan.bearer_token'); + $this->apiKey = config('services.perizinan.api_key'); + } + + /** + * Fetch summary by status from API + */ + public function fetchSummaryByStatus($kategori) + { + try { + $url = "{$this->baseUrl}/summary_by_status"; + + Log::info("Fetching data from API", [ + 'url' => $url, + 'kategori' => $kategori + ]); + + $response = Http::timeout(60) // Increase timeout to 60 seconds + ->retry(3, 100) // Retry 3 times with 100ms delay + ->withHeaders([ + 'Authorization' => 'Bearer ' . $this->bearerToken, + 'x-Gateway-APIKey' => $this->apiKey, + 'Accept' => 'application/json', + 'Content-Type' => 'application/json' + ])->get($url, [ + 'kategori' => $kategori + ]); + + if ($response->successful()) { + $data = $response->json(); + + Log::info("API Response received", [ + 'kategori' => $kategori, + 'success' => $data['success'] ?? false, + 'data_count' => count($data['data'] ?? []) + ]); + + return $data; + } else { + Log::error("API request failed", [ + 'kategori' => $kategori, + 'status' => $response->status(), + 'body' => $response->body() + ]); + + return null; + } + } catch (\Exception $e) { + Log::error("Exception occurred while fetching API data", [ + 'kategori' => $kategori, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return null; + } + } + + /** + * Sync data to database + */ + public function syncToDatabase($kategori) + { + $apiData = $this->fetchSummaryByStatus($kategori); + + if (!$apiData || !($apiData['success'] ?? false)) { + Log::warning("Failed to fetch data for kategori: {$kategori}"); + return false; + } + + $syncDate = Carbon::today(); + $apiLastUpdated = Carbon::parse($apiData['last_updated']); + $syncedCount = 0; + + foreach ($apiData['data'] as $item) { + // Check if data already exists for today + $exists = PerizinanStatus::where('kategori', $kategori) + ->where('status_id', $item['id']) + ->whereDate('sync_date', $syncDate) + ->exists(); + + if (!$exists) { + PerizinanStatus::create([ + 'kategori' => $kategori, + 'status_id' => $item['id'], + 'label' => $item['label'], + 'value' => $item['value'], + 'api_last_updated' => $apiLastUpdated, + 'sync_date' => $syncDate + ]); + + $syncedCount++; + } + } + + Log::info("Data synced successfully", [ + 'kategori' => $kategori, + 'synced_count' => $syncedCount, + 'sync_date' => $syncDate->format('Y-m-d'), + 'api_last_updated' => $apiLastUpdated->format('Y-m-d H:i:s') + ]); + + return true; + } + + /** + * Fetch fastest permohonan from API + */ + public function fetchFastestPermohonan($kategori, $limit = 5) + { + try { + $url = "{$this->baseUrl}/fastest_permohonan"; + + Log::info("Fetching fastest permohonan data from API", [ + 'url' => $url, + 'kategori' => $kategori, + 'limit' => $limit + ]); + + $response = Http::timeout(60) // Increase timeout to 60 seconds + ->retry(3, 100) // Retry 3 times with 100ms delay + ->withHeaders([ + 'Authorization' => 'Bearer ' . $this->bearerToken, + 'x-Gateway-APIKey' => $this->apiKey, + 'Accept' => 'application/json', + 'Content-Type' => 'application/json' + ])->get($url, [ + 'kategori' => $kategori, + 'limit' => $limit + ]); + + if ($response->successful()) { + $data = $response->json(); + + Log::info("Fastest permohonan API Response received", [ + 'kategori' => $kategori, + 'limit' => $limit, + 'success' => $data['success'] ?? false, + 'data_count' => count($data['data'] ?? []) + ]); + + return $data; + } else { + Log::error("Fastest permohonan API request failed", [ + 'kategori' => $kategori, + 'limit' => $limit, + 'status' => $response->status(), + 'body' => $response->body() + ]); + + return null; + } + } catch (\Exception $e) { + Log::error("Exception occurred while fetching fastest permohonan data", [ + 'kategori' => $kategori, + 'limit' => $limit, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return null; + } + } + + /** + * Sync fastest permohonan data to database + */ + public function syncFastestDataToDatabase($kategori, $limit = 5) + { + $apiData = $this->fetchFastestPermohonan($kategori, $limit); + + if (!$apiData || !($apiData['success'] ?? false)) { + Log::warning("Failed to fetch fastest permohonan data for kategori: {$kategori}"); + return false; + } + + $syncDate = Carbon::today(); + $apiLastUpdated = Carbon::parse($apiData['last_updated']); + $syncedCount = 0; + + // Delete existing data for today to ensure we have fresh data + FastestPermohonan::where('kategori', $kategori) + ->whereDate('sync_date', $syncDate) + ->delete(); + + foreach ($apiData['data'] as $index => $item) { + FastestPermohonan::create([ + 'kategori' => $kategori, + 'nama' => $item['nama'], + 'total' => $item['total'], + 'durasi_pemohon' => $item['durasi_pemohon'], + 'durasi_petugas' => $item['durasi_petugas'], + 'api_last_updated' => $apiLastUpdated, + 'sync_date' => $syncDate + ]); + + $syncedCount++; + } + + Log::info("Fastest permohonan data synced successfully", [ + 'kategori' => $kategori, + 'synced_count' => $syncedCount, + 'sync_date' => $syncDate->format('Y-m-d'), + 'api_last_updated' => $apiLastUpdated->format('Y-m-d H:i:s') + ]); + + return true; + } + + /** + * Fetch terakhir terbit from API + */ + public function fetchTerakhirTerbit($kategori, $limit = 5) + { + try { + $url = "{$this->baseUrl}/terakhir_terbit"; + + Log::info("Fetching terakhir terbit data from API", [ + 'url' => $url, + 'kategori' => $kategori, + 'limit' => $limit + ]); + + $response = Http::timeout(60) // Increase timeout to 60 seconds + ->retry(3, 100) // Retry 3 times with 100ms delay + ->withHeaders([ + 'Authorization' => 'Bearer ' . $this->bearerToken, + 'x-Gateway-APIKey' => $this->apiKey, + 'Accept' => 'application/json', + 'Content-Type' => 'application/json' + ])->get($url, [ + 'kategori' => $kategori, + 'limit' => $limit + ]); + + if ($response->successful()) { + $data = $response->json(); + + Log::info("Terakhir terbit API Response received", [ + 'kategori' => $kategori, + 'limit' => $limit, + 'success' => $data['success'] ?? false, + 'data_count' => count($data['data'] ?? []) + ]); + + return $data; + } else { + Log::error("Terakhir terbit API request failed", [ + 'kategori' => $kategori, + 'limit' => $limit, + 'status' => $response->status(), + 'body' => $response->body() + ]); + + return null; + } + } catch (\Exception $e) { + Log::error("Exception occurred while fetching terakhir terbit data", [ + 'kategori' => $kategori, + 'limit' => $limit, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return null; + } + } + + /** + * Sync terakhir terbit data to database + */ + public function syncTerakhirTerbitToDatabase($kategori, $limit = 5) + { + $apiData = $this->fetchTerakhirTerbit($kategori, $limit); + + if (!$apiData || !($apiData['success'] ?? false)) { + Log::warning("Failed to fetch terakhir terbit data for kategori: {$kategori}"); + return false; + } + + $syncDate = Carbon::today(); + $apiLastUpdated = Carbon::parse($apiData['last_updated']); + $syncedCount = 0; + + // Delete existing data for today to ensure we have fresh ranking + TerakhirTerbit::where('kategori', $kategori) + ->whereDate('sync_date', $syncDate) + ->delete(); + + foreach ($apiData['data'] as $index => $item) { + TerakhirTerbit::create([ + 'kategori' => $kategori, + 'nama_izin' => $item['nama_izin'], + 'pemohon' => $item['pemohon'], + 'tanggal_terbit' => Carbon::parse($item['tanggal_terbit']), + 'api_last_updated' => $apiLastUpdated, + 'sync_date' => $syncDate + ]); + + $syncedCount++; + } + + Log::info("Terakhir terbit data synced successfully", [ + 'kategori' => $kategori, + 'synced_count' => $syncedCount, + 'sync_date' => $syncDate->format('Y-m-d'), + 'api_last_updated' => $apiLastUpdated->format('Y-m-d H:i:s') + ]); + + return true; + } + + /** + * Sync all categories + */ + public function syncAllCategories($includeFastest = true, $includeTerakhirTerbit = true) + { + $categories = ['pertek', 'amdal']; + $results = []; + + foreach ($categories as $kategori) { + // Sync status data + $results[$kategori]['status'] = $this->syncToDatabase($kategori); + + // Sync fastest data if requested + if ($includeFastest) { + $results[$kategori]['fastest'] = $this->syncFastestDataToDatabase($kategori, 5); + } + + // Sync terakhir terbit data if requested + if ($includeTerakhirTerbit) { + $results[$kategori]['terakhir_terbit'] = $this->syncTerakhirTerbitToDatabase($kategori, 5); + } + } + + return $results; + } +} diff --git a/check-environment.sh b/check-environment.sh new file mode 100644 index 0000000..c49135d --- /dev/null +++ b/check-environment.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +# Environment Check Script for Production Server +# Run this to verify server requirements + +echo "🔍 Checking Production Environment..." + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +# Check PHP version +echo -e "\n${YELLOW}📋 PHP Version:${NC}" +php_version=$(php -v | head -n 1) +echo "$php_version" + +# Check PHP extensions +echo -e "\n${YELLOW}📦 Required PHP Extensions:${NC}" +extensions=("curl" "json" "mbstring" "openssl" "pdo" "tokenizer" "xml" "zip") + +for ext in "${extensions[@]}"; do + if php -m | grep -q "$ext"; then + echo -e "${GREEN}✅ $ext${NC}" + else + echo -e "${RED}❌ $ext (missing)${NC}" + fi +done + +# Check Composer +echo -e "\n${YELLOW}🎵 Composer:${NC}" +if command -v composer &> /dev/null; then + composer_version=$(composer --version) + echo -e "${GREEN}✅ $composer_version${NC}" +else + echo -e "${RED}❌ Composer not found${NC}" +fi + +# Check cron service +echo -e "\n${YELLOW}⏰ Cron Service:${NC}" +if systemctl is-active --quiet cron; then + echo -e "${GREEN}✅ Cron service is running${NC}" +else + echo -e "${RED}❌ Cron service is not running${NC}" +fi + +# Check disk space +echo -e "\n${YELLOW}💾 Disk Space:${NC}" +df -h | head -n 1 +df -h | grep -E '^/dev/' + +# Check memory +echo -e "\n${YELLOW}🧠 Memory:${NC}" +free -h + +# Check network connectivity to API +echo -e "\n${YELLOW}🌐 API Connectivity:${NC}" +if curl -s --connect-timeout 5 https://wsdev.jakarta.go.id > /dev/null; then + echo -e "${GREEN}✅ Can reach Jakarta API${NC}" +else + echo -e "${RED}❌ Cannot reach Jakarta API${NC}" +fi + +# Check write permissions +echo -e "\n${YELLOW}🔐 Directory Permissions:${NC}" +dirs=("storage" "storage/logs" "bootstrap/cache") + +for dir in "${dirs[@]}"; do + if [ -w "$dir" ]; then + echo -e "${GREEN}✅ $dir is writable${NC}" + else + echo -e "${RED}❌ $dir is not writable${NC}" + fi +done + +echo -e "\n${GREEN}🏁 Environment check completed!${NC}" diff --git a/check_fastest_data.php b/check_fastest_data.php new file mode 100644 index 0000000..bd6c758 --- /dev/null +++ b/check_fastest_data.php @@ -0,0 +1,29 @@ +make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); + +use App\Models\FastestPermohonan; + +echo "=== FASTEST PERMOHONAN DATA ===\n\n"; + +echo "PERTEK Category:\n"; +$pertekData = FastestPermohonan::where('kategori', 'pertek')->orderBy('rank_order')->get(); +foreach ($pertekData as $item) { + echo "#{$item->rank_order}: {$item->nama}\n"; + echo " Total: {$item->total}, Durasi Pemohon: {$item->durasi_pemohon}\n"; + echo " Durasi Petugas: {$item->durasi_petugas}\n\n"; +} + +echo "\nAMDAL Category:\n"; +$amdalData = FastestPermohonan::where('kategori', 'amdal')->orderBy('rank_order')->get(); +foreach ($amdalData as $item) { + echo "#{$item->rank_order}: {$item->nama}\n"; + echo " Total: {$item->total}, Durasi Pemohon: {$item->durasi_pemohon}\n"; + echo " Durasi Petugas: {$item->durasi_petugas}\n\n"; +} + +echo "Total Records: " . FastestPermohonan::count() . "\n"; diff --git a/check_terakhir_terbit.php b/check_terakhir_terbit.php new file mode 100644 index 0000000..dce9370 --- /dev/null +++ b/check_terakhir_terbit.php @@ -0,0 +1,29 @@ +make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); + +use App\Models\TerakhirTerbit; + +echo "=== TERAKHIR TERBIT DATA ===\n\n"; + +echo "PERTEK Category:\n"; +$pertekData = TerakhirTerbit::where('kategori', 'pertek')->orderBy('rank_order')->get(); +foreach ($pertekData as $item) { + echo "#{$item->rank_order}: {$item->nama_izin}\n"; + echo " Pemohon: {$item->pemohon}\n"; + echo " Tanggal Terbit: {$item->tanggal_terbit->format('d M Y')}\n\n"; +} + +echo "\nAMDAL Category:\n"; +$amdalData = TerakhirTerbit::where('kategori', 'amdal')->orderBy('rank_order')->get(); +foreach ($amdalData as $item) { + echo "#{$item->rank_order}: {$item->nama_izin}\n"; + echo " Pemohon: {$item->pemohon}\n"; + echo " Tanggal Terbit: {$item->tanggal_terbit->format('d M Y')}\n\n"; +} + +echo "Total Records: " . TerakhirTerbit::count() . "\n"; diff --git a/config/services.php b/config/services.php index 27a3617..b48291c 100644 --- a/config/services.php +++ b/config/services.php @@ -35,4 +35,10 @@ return [ ], ], + 'perizinan' => [ + 'base_url' => env('PERIZINAN_API_BASE_URL', 'https://wsdev.jakarta.go.id/gateway/DataPerizinanLingkungan/1.0'), + 'bearer_token' => env('PERIZINAN_API_BEARER_TOKEN'), + 'api_key' => env('PERIZINAN_API_KEY'), + ], + ]; diff --git a/database/migrations/2025_07_14_015824_create_perizinan_status_table.php b/database/migrations/2025_07_14_015824_create_perizinan_status_table.php new file mode 100644 index 0000000..95a4c56 --- /dev/null +++ b/database/migrations/2025_07_14_015824_create_perizinan_status_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('kategori'); // pertek, amdal, etc + $table->string('status_id'); // ditolak, selesai, proses, total + $table->string('label'); // Izin Ditolak, Izin Selesai, etc + $table->integer('value'); // count value + $table->datetime('api_last_updated'); // from API last_updated field + $table->date('sync_date'); // date when this data was synced + $table->timestamps(); + + // Index untuk performa + $table->index(['kategori', 'status_id', 'sync_date']); + $table->unique(['kategori', 'status_id', 'sync_date']); // prevent duplicate data per day + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('perizinan_status'); + } +}; diff --git a/database/migrations/2025_07_14_024445_create_fastest_permohonan_table.php b/database/migrations/2025_07_14_024445_create_fastest_permohonan_table.php new file mode 100644 index 0000000..8984c5f --- /dev/null +++ b/database/migrations/2025_07_14_024445_create_fastest_permohonan_table.php @@ -0,0 +1,38 @@ +id(); + $table->string('kategori'); // pertek, amdal, etc + $table->string('nama'); // nama izin from API + $table->integer('total'); // jumlah total izin from API + $table->string('durasi_pemohon')->nullable(); // durasi rata-rata pemohon from API + $table->string('durasi_petugas')->nullable(); // durasi rata-rata petugas from API + $table->datetime('api_last_updated'); // from API last_updated field + $table->date('sync_date'); // date when this data was synced + $table->timestamps(); + + // Index untuk performa - urutkan berdasarkan durasi dan total + $table->index(['kategori', 'sync_date']); + $table->index(['kategori', 'total']); // untuk sorting fastest + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('fastest_permohonan'); + } +}; diff --git a/database/migrations/2025_07_14_033547_create_terakhir_terbit_table.php b/database/migrations/2025_07_14_033547_create_terakhir_terbit_table.php new file mode 100644 index 0000000..3e9086e --- /dev/null +++ b/database/migrations/2025_07_14_033547_create_terakhir_terbit_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('kategori'); // pertek, amdal, etc + $table->string('nama_izin'); // nama izin dari API + $table->string('pemohon'); // nama pemohon dari API + $table->date('tanggal_terbit'); // tanggal terbit dari API + $table->datetime('api_last_updated'); // from API last_updated field + $table->date('sync_date'); // date when this data was synced + $table->timestamps(); + + // Index untuk performa + $table->index(['kategori', 'tanggal_terbit']); + $table->index(['kategori', 'sync_date']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('terakhir_terbit'); + } +}; diff --git a/debug_api.php b/debug_api.php new file mode 100644 index 0000000..b327182 --- /dev/null +++ b/debug_api.php @@ -0,0 +1,22 @@ +make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); + +use App\Services\PerizinanApiService; + +$service = new PerizinanApiService(); +$data = $service->fetchFastestPermohonan('pertek', 5); + +echo "API Response:\n"; +print_r($data); + +if ($data && isset($data['data']) && is_array($data['data'])) { + echo "\nFirst item structure:\n"; + if (count($data['data']) > 0) { + print_r($data['data'][0]); + } +} diff --git a/deploy-production.sh b/deploy-production.sh new file mode 100644 index 0000000..3ccab31 --- /dev/null +++ b/deploy-production.sh @@ -0,0 +1,109 @@ +#!/bin/bash + +# Production Deployment Script for Perizinan System +# Run this script after deploying to production server + +echo "🚀 Setting up Perizinan API Integration for Production..." + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Get current directory +CURRENT_DIR=$(pwd) +echo -e "${BLUE}Current directory: $CURRENT_DIR${NC}" + +# Check if we're in Laravel project +if [ ! -f "artisan" ]; then + echo -e "${RED}❌ Error: artisan file not found. Make sure you're in Laravel project directory.${NC}" + exit 1 +fi + +# 1. Make shell scripts executable +echo -e "${YELLOW}📁 Making shell scripts executable...${NC}" +chmod +x sync-perizinan.sh +chmod +x run-scheduler.sh +echo -e "${GREEN}✅ Shell scripts are now executable${NC}" + +# 2. Create log directories if they don't exist +echo -e "${YELLOW}📁 Creating log directories...${NC}" +mkdir -p storage/logs +echo -e "${GREEN}✅ Log directories created${NC}" + +# 3. Set proper permissions +echo -e "${YELLOW}🔐 Setting proper permissions...${NC}" +chmod -R 775 storage/ +chmod -R 775 bootstrap/cache/ +echo -e "${GREEN}✅ Permissions set${NC}" + +# 4. Update .env for production +echo -e "${YELLOW}⚙️ Checking .env configuration...${NC}" +if [ ! -f ".env" ]; then + echo -e "${RED}❌ .env file not found. Please create it first.${NC}" + exit 1 +fi + +# Check if API credentials are set +if grep -q "PERIZINAN_API_BEARER_TOKEN=your_actual_bearer_token" .env; then + echo -e "${RED}⚠️ Warning: Please update PERIZINAN_API_BEARER_TOKEN in .env${NC}" +fi + +if grep -q "PERIZINAN_API_KEY=your_actual_api_key" .env; then + echo -e "${RED}⚠️ Warning: Please update PERIZINAN_API_KEY in .env${NC}" +fi + +# 5. Run database migrations +echo -e "${YELLOW}🗄️ Running database migrations...${NC}" +php artisan migrate --force +echo -e "${GREEN}✅ Database migrations completed${NC}" + +# 6. Test API connection +echo -e "${YELLOW}🌐 Testing API connection...${NC}" +php artisan perizinan:sync pertek +if [ $? -eq 0 ]; then + echo -e "${GREEN}✅ API connection test successful${NC}" +else + echo -e "${RED}❌ API connection test failed. Please check your credentials.${NC}" +fi + +# 7. Setup cron job options +echo "" +echo -e "${BLUE}📅 Cron Job Setup Options:${NC}" +echo "" +echo -e "${YELLOW}Option 1: Laravel Scheduler (Recommended)${NC}" +echo "Add this line to crontab (crontab -e):" +echo -e "${GREEN}* * * * * cd $CURRENT_DIR && ./run-scheduler.sh >/dev/null 2>&1${NC}" +echo "" +echo -e "${YELLOW}Option 2: Direct Sync${NC}" +echo "Add this line to crontab (crontab -e):" +echo -e "${GREEN}0 0 * * * cd $CURRENT_DIR && ./sync-perizinan.sh >/dev/null 2>&1${NC}" +echo "" + +# 8. Show commands for manual setup +echo -e "${BLUE}📋 Manual Commands:${NC}" +echo "" +echo "To edit crontab:" +echo -e "${GREEN}sudo crontab -e${NC}" +echo "" +echo "To view current crontab:" +echo -e "${GREEN}crontab -l${NC}" +echo "" +echo "To test sync manually:" +echo -e "${GREEN}php artisan perizinan:sync${NC}" +echo "" +echo "To check synced data:" +echo -e "${GREEN}php artisan perizinan:check${NC}" +echo "" +echo "To view logs:" +echo -e "${GREEN}tail -f storage/logs/cron.log${NC}" +echo -e "${GREEN}tail -f storage/logs/laravel.log${NC}" +echo "" + +echo -e "${GREEN}🎉 Production setup completed!${NC}" +echo -e "${YELLOW}📝 Don't forget to:${NC}" +echo "1. Update API credentials in .env" +echo "2. Setup cron job using one of the options above" +echo "3. Monitor logs after deployment" diff --git a/resources/views/dashboard/index.blade.php b/resources/views/dashboard/index.blade.php index 84ccc20..90d9a7c 100644 --- a/resources/views/dashboard/index.blade.php +++ b/resources/views/dashboard/index.blade.php @@ -112,7 +112,13 @@
PERTEK

Total Pengajuan

-

{{ $statuses[3]['value'] ?? 65 }}

+

+ @php + $pertekData = $allStatistics['pertek']['data'] ?? []; + $totalData = collect($pertekData)->firstWhere('id', 'total'); + @endphp + {{ number_format($totalData['value'] ?? 65) }} +

@@ -130,7 +136,12 @@
PERTEK

Izin Disetujui

-

{{ $statuses[1]['value'] ?? 45 }}

+

+ @php + $selesaiData = collect($pertekData)->firstWhere('id', 'selesai'); + @endphp + {{ number_format($selesaiData['value'] ?? 45) }} +

@@ -149,7 +160,12 @@
PERTEK

Dalam Proses

-

{{ $statuses[2]['value'] ?? 12 }}

+

+ @php + $prosesData = collect($pertekData)->firstWhere('id', 'proses'); + @endphp + {{ number_format($prosesData['value'] ?? 12) }} +

@@ -168,7 +184,12 @@
PERTEK

Izin Ditolak

-

{{ $statuses[0]['value'] ?? 8 }}

+

+ @php + $ditolakData = collect($pertekData)->firstWhere('id', 'ditolak'); + @endphp + {{ number_format($ditolakData['value'] ?? 8) }} +

@@ -199,6 +220,18 @@ + @php + $pertekFastestData = $allFastestData['pertek']['data'] ?? []; + @endphp + @forelse($pertekFastestData as $fastest) + + {{ $fastest['rank_order'] }} + {{ $fastest['nama'] }} + {{ number_format($fastest['total']) }} + {{ $fastest['durasi_pemohon'] }} + {{ $fastest['durasi_petugas'] }} + + @empty 1 SERTIFIKAT LAIK OPERASI - PEMENUHAN BAKU MUTU EMISI @@ -234,6 +267,7 @@ 55 Jam 12 Menit 45 Jam 36 Menit + @endforelse
@@ -257,6 +291,17 @@ + @php + $pertekTerakhirTerbitData = $allTerakhirTerbitData['pertek']['data'] ?? []; + @endphp + @forelse($pertekTerakhirTerbitData as $terakhir) + + {{ $terakhir['rank_order'] }} + {{ $terakhir['nama_izin'] }} + {{ $terakhir['pemohon'] }} + {{ \App\Helpers\DashboardHelper::formatTanggalTerbit($terakhir['tanggal_terbit']) }} + + @empty 1 SERTIFIKAT LAIK OPERASI - PEMENUHAN BAKU MUTU EMISI @@ -287,6 +332,7 @@ CV. BERKAH JAYA 11 Des 2024 + @endforelse
@@ -377,7 +423,13 @@
RINTEK

Total Pengajuan

-

892

+

+ @php + $rintekData = $allStatistics['rintek']['data'] ?? []; + $rintekTotalData = collect($rintekData)->firstWhere('id', 'total'); + @endphp + {{ number_format($rintekTotalData['value'] ?? 892) }} +

@@ -395,7 +447,12 @@
RINTEK

Izin Disetujui

-

675

+

+ @php + $rintekSelesaiData = collect($rintekData)->firstWhere('id', 'selesai'); + @endphp + {{ number_format($rintekSelesaiData['value'] ?? 675) }} +

@@ -413,7 +470,12 @@
RINTEK

Dalam Proses

-

147

+

+ @php + $rintekProsesData = collect($rintekData)->firstWhere('id', 'proses'); + @endphp + {{ number_format($rintekProsesData['value'] ?? 147) }} +

@@ -432,7 +494,12 @@
RINTEK

Izin Ditolak

-

70

+

+ @php + $rintekDitolakData = collect($rintekData)->firstWhere('id', 'ditolak'); + @endphp + {{ number_format($rintekDitolakData['value'] ?? 70) }} +

@@ -641,7 +708,13 @@
AMDAL

Total Pengajuan

-

456

+

+ @php + $amdalData = $allStatistics['amdal']['data'] ?? []; + $amdalTotalData = collect($amdalData)->firstWhere('id', 'total'); + @endphp + {{ number_format($amdalTotalData['value'] ?? 456) }} +

@@ -659,7 +732,12 @@
AMDAL

Izin Disetujui

-

298

+

+ @php + $amdalSelesaiData = collect($amdalData)->firstWhere('id', 'selesai'); + @endphp + {{ number_format($amdalSelesaiData['value'] ?? 298) }} +

@@ -677,7 +755,12 @@
AMDAL

Dalam Proses

-

112

+

+ @php + $amdalProsesData = collect($amdalData)->firstWhere('id', 'proses'); + @endphp + {{ number_format($amdalProsesData['value'] ?? 112) }} +

@@ -696,7 +779,12 @@
AMDAL

Izin Ditolak

-

46

+

+ @php + $amdalDitolakData = collect($amdalData)->firstWhere('id', 'ditolak'); + @endphp + {{ number_format($amdalDitolakData['value'] ?? 46) }} +

@@ -727,6 +815,18 @@ + @php + $amdalFastestData = $allFastestData['amdal']['data'] ?? []; + @endphp + @forelse($amdalFastestData as $fastest) + + {{ $fastest['rank_order'] }} + {{ $fastest['nama'] }} + {{ number_format($fastest['total']) }} + {{ $fastest['durasi_pemohon'] }} + {{ $fastest['durasi_petugas'] }} + + @empty 1 IZIN LINGKUNGAN KEGIATAN USAHA @@ -762,6 +862,7 @@ 84 Jam 50 Menit 58 Jam 25 Menit + @endforelse
@@ -785,6 +886,17 @@ + @php + $amdalTerakhirTerbitData = $allTerakhirTerbitData['amdal']['data'] ?? []; + @endphp + @forelse($amdalTerakhirTerbitData as $terakhir) + + {{ $terakhir['rank_order'] }} + {{ $terakhir['nama_izin'] }} + {{ $terakhir['pemohon'] }} + {{ \App\Helpers\DashboardHelper::formatTanggalTerbit($terakhir['tanggal_terbit']) }} + + @empty 1 SERTIFIKAT LAIK OPERASI - PEMENUHAN BAKU MUTU EMISI @@ -815,6 +927,7 @@ CV. BERKAH JAYA 11 Des 2024 + @endforelse
diff --git a/routes/console.php b/routes/console.php index 19cef70..1ecc7eb 100644 --- a/routes/console.php +++ b/routes/console.php @@ -3,8 +3,16 @@ use Illuminate\Foundation\Console\ClosureCommand; use Illuminate\Foundation\Inspiring; use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\Schedule; Artisan::command('inspire', function () { /** @var ClosureCommand $this */ $this->comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); + +// Schedule perizinan data sync daily at midnight (including all data types) +Schedule::command('perizinan:sync --all') + ->dailyAt('00:00') + ->timezone('Asia/Jakarta') + ->withoutOverlapping() + ->runInBackground(); diff --git a/run-scheduler.bat b/run-scheduler.bat new file mode 100644 index 0000000..7934e55 --- /dev/null +++ b/run-scheduler.bat @@ -0,0 +1,8 @@ +@echo off +REM Laravel Scheduler Runner +REM This should run every minute via Task Scheduler + +cd /d "C:\laragon\www\perling" + +REM Run Laravel scheduler +php artisan schedule:run >> storage\logs\scheduler.log 2>&1 diff --git a/run-scheduler.sh b/run-scheduler.sh new file mode 100644 index 0000000..bd43a5a --- /dev/null +++ b/run-scheduler.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# Laravel Scheduler Runner for Linux Server +# This should run every minute via cron + +# Set project directory (adjust this path for your production server) +PROJECT_DIR="/var/www/perling" + +# Change to project directory +cd "$PROJECT_DIR" + +# Run Laravel scheduler +php artisan schedule:run >> storage/logs/scheduler.log 2>&1 diff --git a/setup-task-scheduler.ps1 b/setup-task-scheduler.ps1 new file mode 100644 index 0000000..ffc28f9 --- /dev/null +++ b/setup-task-scheduler.ps1 @@ -0,0 +1,52 @@ +# PowerShell Script to Create Windows Task Scheduler for Perizinan Data Sync +# Run this script as Administrator + +$TaskName = "PerizinanDataSync" +$TaskDescription = "Daily sync of perizinan data from Jakarta API at midnight" +$ScriptPath = "C:\laragon\www\perling\sync-perizinan.bat" +$LogPath = "C:\laragon\www\perling\storage\logs\task-scheduler.log" + +Write-Host "Creating Windows Task Scheduler for Perizinan Data Sync..." -ForegroundColor Green + +# Check if task already exists +$ExistingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue + +if ($ExistingTask) { + Write-Host "Task '$TaskName' already exists. Removing old task..." -ForegroundColor Yellow + Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false +} + +try { + # Create Task Action + $Action = New-ScheduledTaskAction -Execute $ScriptPath + + # Create Task Trigger (Daily at midnight) + $Trigger = New-ScheduledTaskTrigger -Daily -At "00:00" + + # Create Task Settings + $Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable + + # Create Task Principal (Run with highest privileges) + $Principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest + + # Register the Task + Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $Trigger -Settings $Settings -Principal $Principal -Description $TaskDescription + + Write-Host "✅ Task Scheduler created successfully!" -ForegroundColor Green + Write-Host "Task Name: $TaskName" -ForegroundColor Cyan + Write-Host "Schedule: Daily at 00:00 (midnight)" -ForegroundColor Cyan + Write-Host "Script Path: $ScriptPath" -ForegroundColor Cyan + + # Show task info + Get-ScheduledTask -TaskName $TaskName | Format-Table -AutoSize + +} catch { + Write-Host "❌ Error creating task scheduler: $($_.Exception.Message)" -ForegroundColor Red + Write-Host "Please run this script as Administrator" -ForegroundColor Yellow +} + +Write-Host "`nTo manually run the task, use:" -ForegroundColor Cyan +Write-Host "Start-ScheduledTask -TaskName '$TaskName'" -ForegroundColor White + +Write-Host "`nTo view task history:" -ForegroundColor Cyan +Write-Host "Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-TaskScheduler/Operational'; ID=102}" -ForegroundColor White diff --git a/sync-perizinan.bat b/sync-perizinan.bat new file mode 100644 index 0000000..fe51021 --- /dev/null +++ b/sync-perizinan.bat @@ -0,0 +1,15 @@ +@echo off +REM Perizinan Data Sync Script +REM Runs daily at midnight to sync data from API + +cd /d "C:\laragon\www\perling" + +REM Log start time +echo [%date% %time%] Starting perizinan data sync >> storage\logs\cron.log + +REM Run the sync command +php artisan perizinan:sync >> storage\logs\cron.log 2>&1 + +REM Log completion +echo [%date% %time%] Perizinan data sync completed >> storage\logs\cron.log +echo. >> storage\logs\cron.log diff --git a/sync-perizinan.sh b/sync-perizinan.sh new file mode 100644 index 0000000..5da9eb0 --- /dev/null +++ b/sync-perizinan.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Perizinan Data Sync Script for Linux Server +# Runs daily at midnight to sync data from API + +# Set project directory (adjust this path for your production server) +PROJECT_DIR="/var/www/perling" + +# Change to project directory +cd "$PROJECT_DIR" + +# Log start time +echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting perizinan data sync" >> storage/logs/cron.log + +# Run the sync command +php artisan perizinan:sync >> storage/logs/cron.log 2>&1 + +# Log completion +echo "[$(date '+%Y-%m-%d %H:%M:%S')] Perizinan data sync completed" >> storage/logs/cron.log +echo "" >> storage/logs/cron.log + +# Optional: Send notification if sync fails +# if [ $? -ne 0 ]; then +# echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Perizinan sync failed" >> storage/logs/cron.log +# fi diff --git a/test_all_tables.php b/test_all_tables.php new file mode 100644 index 0000000..f5220eb --- /dev/null +++ b/test_all_tables.php @@ -0,0 +1,35 @@ +make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); + +use App\Helpers\DashboardHelper; + +echo "=== TESTING ALL DATA TABLES AFTER RANK_ORDER REMOVAL ===\n\n"; + +// Test fastest data +echo "FASTEST DATA (API ORDER WITHOUT RANK_ORDER):\n"; +$fastestData = DashboardHelper::getAllFastestData(); +foreach ($fastestData as $type => $data) { + echo "{$data['label']} - Top 5 Tercepat:\n"; + foreach ($data['data'] as $item) { + echo " #{$item['rank_order']}: {$item['nama']}\n"; + echo " Total: " . number_format($item['total']) . ", Durasi Pemohon: {$item['durasi_pemohon']}\n"; + } + echo "\n"; +} + +// Test terakhir terbit data +echo "TERAKHIR TERBIT DATA (ORDERED BY tanggal_terbit):\n"; +$terakhirTerbitData = DashboardHelper::getAllTerakhirTerbitData(); +foreach ($terakhirTerbitData as $type => $data) { + echo "{$data['label']} - Top 5 Terakhir Terbit:\n"; + foreach ($data['data'] as $item) { + echo " #{$item['rank_order']}: {$item['nama_izin']}\n"; + echo " Pemohon: {$item['pemohon']}, Tanggal: " . DashboardHelper::formatTanggalTerbit($item['tanggal_terbit']) . "\n"; + } + echo "\n"; +} diff --git a/test_dashboard_helper.php b/test_dashboard_helper.php new file mode 100644 index 0000000..e69de29 diff --git a/test_terakhir_terbit.php b/test_terakhir_terbit.php new file mode 100644 index 0000000..f3fdf8f --- /dev/null +++ b/test_terakhir_terbit.php @@ -0,0 +1,31 @@ +make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); + +use App\Models\TerakhirTerbit; + +echo "=== TERAKHIR TERBIT DATA (ORDERED BY tanggal_terbit DESC) ===\n\n"; + +echo "PERTEK:\n"; +$pertekData = TerakhirTerbit::getLatestByKategori('pertek'); +foreach ($pertekData as $index => $item) { + $no = $index + 1; + echo " {$no}. {$item->nama_izin}\n"; + echo " Pemohon: {$item->pemohon}\n"; + echo " Tanggal Terbit: {$item->tanggal_terbit->format('d M Y')}\n"; + echo " Sync Date: {$item->sync_date->format('Y-m-d')}\n\n"; +} + +echo "AMDAL:\n"; +$amdalData = TerakhirTerbit::getLatestByKategori('amdal'); +foreach ($amdalData as $index => $item) { + $no = $index + 1; + echo " {$no}. {$item->nama_izin}\n"; + echo " Pemohon: {$item->pemohon}\n"; + echo " Tanggal Terbit: {$item->tanggal_terbit->format('d M Y')}\n"; + echo " Sync Date: {$item->sync_date->format('Y-m-d')}\n\n"; +}