initial commit

main
marszayn 2025-07-21 08:53:14 +07:00
parent d78647ce46
commit 266632ec35
35 changed files with 2617 additions and 29 deletions

5
.gitignore vendored
View File

@ -18,6 +18,11 @@ yarn-error.log
/auth.json /auth.json
/.fleet /.fleet
/.idea /.idea
# Production logs (but keep the structure)
/storage/logs/*.log
!/storage/logs/.gitkeep
/.nova /.nova
/.vscode /.vscode
/.zed /.zed

View File

@ -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.

View File

@ -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 <token>`, `x-Gateway-APIKey: <key>`
## 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

View File

@ -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!**

View File

@ -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
```

View File

@ -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

View File

@ -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`

View File

@ -0,0 +1,38 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\PerizinanStatus;
class CheckPerizinanData extends Command
{
protected $signature = 'perizinan:check';
protected $description = 'Check synced perizinan data';
public function handle()
{
$this->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());
}
}

View File

@ -0,0 +1,154 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\PerizinanApiService;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
class SyncPerizinanData extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'perizinan:sync {kategori?} {--fastest : Include fastest permohonan data sync} {--terakhir-terbit : Include terakhir terbit data sync} {--all : Include all data types (fastest + terakhir terbit)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sync perizinan data from API to database (status, fastest permohonan, and terakhir terbit)';
/**
* Execute the console command.
*/
public function handle()
{
$startTime = Carbon::now();
$kategori = $this->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;
}
}

View File

@ -0,0 +1,122 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\PerizinanStatus;
use App\Services\PerizinanApiService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Artisan;
use Carbon\Carbon;
class TestProductionSetup extends Command
{
protected $signature = 'perizinan:test-production';
protected $description = 'Test production setup readiness';
public function handle()
{
$this->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;
}
}

View File

@ -2,12 +2,17 @@
namespace App\Helpers; namespace App\Helpers;
use App\Models\PerizinanStatus;
use App\Models\FastestPermohonan;
use App\Models\TerakhirTerbit;
use Carbon\Carbon;
class DashboardHelper 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 = [ $data = [
'pertek' => [ 'pertek' => [
@ -15,16 +20,16 @@ class DashboardHelper
['id' => 'selesai', 'label' => 'Izin Selesai', 'value' => 45, 'color' => 'success'], ['id' => 'selesai', 'label' => 'Izin Selesai', 'value' => 45, 'color' => 'success'],
['id' => 'proses', 'label' => 'Dalam Proses', 'value' => 12, 'color' => 'info'], ['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' => [ 'amdal' => [
['id' => 'ditolak', 'label' => 'Izin Ditolak', 'value' => 3, 'color' => 'danger'], ['id' => 'ditolak', 'label' => 'Izin Ditolak', 'value' => 3, 'color' => 'danger'],
['id' => 'selesai', 'label' => 'Izin Selesai', 'value' => 15, 'color' => 'success'], ['id' => 'selesai', 'label' => 'Izin Selesai', 'value' => 15, 'color' => 'success'],
['id' => 'proses', 'label' => 'Dalam Proses', 'value' => 9, 'color' => 'info'], ['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' => [ 'izin_angkut' => [
['id' => 'ditolak', 'label' => 'Izin Ditolak', 'value' => 12, 'color' => 'danger'], ['id' => 'ditolak', 'label' => 'Izin Ditolak', 'value' => 12, 'color' => 'danger'],
['id' => 'selesai', 'label' => 'Izin Selesai', 'value' => 67, 'color' => 'success'], ['id' => 'selesai', 'label' => 'Izin Selesai', 'value' => 67, 'color' => 'success'],
@ -40,18 +45,56 @@ class DashboardHelper
return $data[$type] ?? []; 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 * Menambahkan total ke data statuses
*/ */
public static function addTotalToStatuses(array $statuses): array public static function addTotalToStatuses(array $statuses): array
{ {
$total = array_sum(array_column($statuses, 'value')); // Cek apakah sudah ada total di data
$statuses[] = [ $hasTotal = collect($statuses)->contains('id', 'total');
'id' => 'total',
'label' => 'Total Pengajuan', if (!$hasTotal) {
'value' => $total, $total = array_sum(array_column($statuses, 'value'));
'color' => 'primary' $statuses[] = [
]; 'id' => 'total',
'label' => 'Total Pengajuan',
'value' => $total,
'color' => 'primary'
];
}
return $statuses; return $statuses;
} }
@ -224,4 +267,181 @@ class DashboardHelper
return $colors[$statusId] ?? '#6b7280'; 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');
}
} }

View File

@ -8,14 +8,23 @@ use Illuminate\Support\Facades\Log;
use App\Helpers\DashboardHelper; use App\Helpers\DashboardHelper;
class DashboardController extends Controller 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::getStatusDataByType('pertek');
$statuses = DashboardHelper::addTotalToStatuses($statuses); $statuses = DashboardHelper::addTotalToStatuses($statuses);
$type = 'pertek'; $type = 'pertek';
return view('dashboard/index', compact('statuses', 'type')); return view('dashboard/index', compact('statuses', 'type', 'allStatistics', 'allFastestData', 'allTerakhirTerbitData'));
} }
public function pertek() public function pertek()

View File

@ -0,0 +1,54 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
class FastestPermohonan extends Model
{
protected $table = 'fastest_permohonan';
protected $fillable = [
'kategori',
'nama',
'total',
'durasi_pemohon',
'durasi_petugas',
'api_last_updated',
'sync_date'
];
protected $casts = [
'api_last_updated' => '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();
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
class PerizinanStatus extends Model
{
protected $table = 'perizinan_status';
protected $fillable = [
'kategori',
'status_id',
'label',
'value',
'api_last_updated',
'sync_date'
];
protected $casts = [
'api_last_updated' => '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');
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
class TerakhirTerbit extends Model
{
protected $table = 'terakhir_terbit';
protected $fillable = [
'kategori',
'nama_izin',
'pemohon',
'tanggal_terbit',
'api_last_updated',
'sync_date'
];
protected $casts = [
'tanggal_terbit' => '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();
}
}

View File

@ -0,0 +1,358 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use App\Models\PerizinanStatus;
use App\Models\FastestPermohonan;
use App\Models\TerakhirTerbit;
use Carbon\Carbon;
class PerizinanApiService
{
private $baseUrl;
private $bearerToken;
private $apiKey;
public function __construct()
{
$this->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;
}
}

View File

@ -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}"

View File

@ -0,0 +1,29 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
$app = require_once __DIR__ . '/bootstrap/app.php';
$app->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";

View File

@ -0,0 +1,29 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
$app = require_once __DIR__ . '/bootstrap/app.php';
$app->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";

View File

@ -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'),
],
]; ];

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('perizinan_status', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('fastest_permohonan', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('terakhir_terbit', function (Blueprint $table) {
$table->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');
}
};

22
debug_api.php 100644
View File

@ -0,0 +1,22 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
$app = require_once __DIR__ . '/bootstrap/app.php';
$app->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]);
}
}

View File

@ -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"

View File

@ -112,7 +112,13 @@
<div> <div>
<span class="badge text-bg-primary">PERTEK</span> <span class="badge text-bg-primary">PERTEK</span>
<p class="fw-medium text-primary-light mb-1">Total Pengajuan</p> <p class="fw-medium text-primary-light mb-1">Total Pengajuan</p>
<h4 class="mb-1 fw-bold text-primary-600">{{ $statuses[3]['value'] ?? 65 }}</h4> <h4 class="mb-1 fw-bold text-primary-600">
@php
$pertekData = $allStatistics['pertek']['data'] ?? [];
$totalData = collect($pertekData)->firstWhere('id', 'total');
@endphp
{{ number_format($totalData['value'] ?? 65) }}
</h4>
</div> </div>
<div class="w-50-px h-50-px bg-yellow rounded-circle d-flex justify-content-center align-items-center"> <div class="w-50-px h-50-px bg-yellow rounded-circle d-flex justify-content-center align-items-center">
<x-lucide-equal class="text-white w-32-px h-32-px mb-0"/> <x-lucide-equal class="text-white w-32-px h-32-px mb-0"/>
@ -130,7 +136,12 @@
<div> <div>
<span class="badge text-bg-primary">PERTEK</span> <span class="badge text-bg-primary">PERTEK</span>
<p class="fw-medium text-primary-light mb-1">Izin Disetujui</p> <p class="fw-medium text-primary-light mb-1">Izin Disetujui</p>
<h4 class="mb-1 fw-bold text-success-main">{{ $statuses[1]['value'] ?? 45 }}</h4> <h4 class="mb-1 fw-bold text-success-main">
@php
$selesaiData = collect($pertekData)->firstWhere('id', 'selesai');
@endphp
{{ number_format($selesaiData['value'] ?? 45) }}
</h4>
</div> </div>
<div class="w-50-px h-50-px bg-green rounded-circle d-flex justify-content-center align-items-center"> <div class="w-50-px h-50-px bg-green rounded-circle d-flex justify-content-center align-items-center">
@ -149,7 +160,12 @@
<div> <div>
<span class="badge text-bg-primary">PERTEK</span> <span class="badge text-bg-primary">PERTEK</span>
<p class="fw-medium text-primary-light mb-1">Dalam Proses</p> <p class="fw-medium text-primary-light mb-1">Dalam Proses</p>
<h4 class="mb-1 fw-bold text-info-main">{{ $statuses[2]['value'] ?? 12 }}</h4> <h4 class="mb-1 fw-bold text-info-main">
@php
$prosesData = collect($pertekData)->firstWhere('id', 'proses');
@endphp
{{ number_format($prosesData['value'] ?? 12) }}
</h4>
</div> </div>
<div class="w-50-px h-50-px bg-info rounded-circle d-flex justify-content-center align-items-center"> <div class="w-50-px h-50-px bg-info rounded-circle d-flex justify-content-center align-items-center">
@ -168,7 +184,12 @@
<div> <div>
<span class="badge text-bg-primary">PERTEK</span> <span class="badge text-bg-primary">PERTEK</span>
<p class="fw-medium text-primary-light mb-1">Izin Ditolak</p> <p class="fw-medium text-primary-light mb-1">Izin Ditolak</p>
<h4 class="mb-1 fw-bold text-danger-main">{{ $statuses[0]['value'] ?? 8 }}</h4> <h4 class="mb-1 fw-bold text-danger-main">
@php
$ditolakData = collect($pertekData)->firstWhere('id', 'ditolak');
@endphp
{{ number_format($ditolakData['value'] ?? 8) }}
</h4>
</div> </div>
<div class="w-50-px h-50-px bg-red rounded-circle d-flex justify-content-center align-items-center"> <div class="w-50-px h-50-px bg-red rounded-circle d-flex justify-content-center align-items-center">
@ -199,6 +220,18 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@php
$pertekFastestData = $allFastestData['pertek']['data'] ?? [];
@endphp
@forelse($pertekFastestData as $fastest)
<tr>
<td>{{ $fastest['rank_order'] }}</td>
<td class="text-sm">{{ $fastest['nama'] }}</td>
<td class="text-sm d-none d-md-table-cell">{{ number_format($fastest['total']) }}</td>
<td class="text-sm d-none d-xl-table-cell">{{ $fastest['durasi_pemohon'] }}</td>
<td class="text-sm d-none d-xl-table-cell">{{ $fastest['durasi_petugas'] }}</td>
</tr>
@empty
<tr> <tr>
<td>1</td> <td>1</td>
<td class="text-sm">SERTIFIKAT LAIK OPERASI - PEMENUHAN BAKU MUTU EMISI</td> <td class="text-sm">SERTIFIKAT LAIK OPERASI - PEMENUHAN BAKU MUTU EMISI</td>
@ -234,6 +267,7 @@
<td class="text-sm d-none d-xl-table-cell">55 Jam 12 Menit</td> <td class="text-sm d-none d-xl-table-cell">55 Jam 12 Menit</td>
<td class="text-sm d-none d-xl-table-cell">45 Jam 36 Menit</td> <td class="text-sm d-none d-xl-table-cell">45 Jam 36 Menit</td>
</tr> </tr>
@endforelse
</tbody> </tbody>
</table> </table>
</div> </div>
@ -257,6 +291,17 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@php
$pertekTerakhirTerbitData = $allTerakhirTerbitData['pertek']['data'] ?? [];
@endphp
@forelse($pertekTerakhirTerbitData as $terakhir)
<tr>
<td>{{ $terakhir['rank_order'] }}</td>
<td class="text-sm">{{ $terakhir['nama_izin'] }}</td>
<td class="text-sm d-none d-md-table-cell">{{ $terakhir['pemohon'] }}</td>
<td class="text-sm d-none d-xl-table-cell">{{ \App\Helpers\DashboardHelper::formatTanggalTerbit($terakhir['tanggal_terbit']) }}</td>
</tr>
@empty
<tr> <tr>
<td>1</td> <td>1</td>
<td class="text-sm">SERTIFIKAT LAIK OPERASI - PEMENUHAN BAKU MUTU EMISI</td> <td class="text-sm">SERTIFIKAT LAIK OPERASI - PEMENUHAN BAKU MUTU EMISI</td>
@ -287,6 +332,7 @@
<td class="text-sm d-none d-md-table-cell">CV. BERKAH JAYA</td> <td class="text-sm d-none d-md-table-cell">CV. BERKAH JAYA</td>
<td class="text-sm d-none d-xl-table-cell">11 Des 2024</td> <td class="text-sm d-none d-xl-table-cell">11 Des 2024</td>
</tr> </tr>
@endforelse
</tbody> </tbody>
</table> </table>
</div> </div>
@ -377,7 +423,13 @@
<div> <div>
<span class="badge text-bg-success">RINTEK</span> <span class="badge text-bg-success">RINTEK</span>
<p class="fw-medium text-primary-light mb-1">Total Pengajuan</p> <p class="fw-medium text-primary-light mb-1">Total Pengajuan</p>
<h4 class="mb-1 fw-bold text-primary-600">892</h4> <h4 class="mb-1 fw-bold text-primary-600">
@php
$rintekData = $allStatistics['rintek']['data'] ?? [];
$rintekTotalData = collect($rintekData)->firstWhere('id', 'total');
@endphp
{{ number_format($rintekTotalData['value'] ?? 892) }}
</h4>
</div> </div>
<div class="w-50-px h-50-px bg-yellow rounded-circle d-flex justify-content-center align-items-center"> <div class="w-50-px h-50-px bg-yellow rounded-circle d-flex justify-content-center align-items-center">
<x-lucide-equal class="text-white w-32-px h-32-px mb-0"/> <x-lucide-equal class="text-white w-32-px h-32-px mb-0"/>
@ -395,7 +447,12 @@
<div> <div>
<span class="badge text-bg-success">RINTEK</span> <span class="badge text-bg-success">RINTEK</span>
<p class="fw-medium text-primary-light mb-1">Izin Disetujui</p> <p class="fw-medium text-primary-light mb-1">Izin Disetujui</p>
<h4 class="mb-1 fw-bold text-success-main">675</h4> <h4 class="mb-1 fw-bold text-success-main">
@php
$rintekSelesaiData = collect($rintekData)->firstWhere('id', 'selesai');
@endphp
{{ number_format($rintekSelesaiData['value'] ?? 675) }}
</h4>
</div> </div>
<div class="w-50-px h-50-px bg-green rounded-circle d-flex justify-content-center align-items-center"> <div class="w-50-px h-50-px bg-green rounded-circle d-flex justify-content-center align-items-center">
<x-lucide-circle-check class="text-white w-32-px h-32-px mb-0"/> <x-lucide-circle-check class="text-white w-32-px h-32-px mb-0"/>
@ -413,7 +470,12 @@
<div> <div>
<span class="badge text-bg-success">RINTEK</span> <span class="badge text-bg-success">RINTEK</span>
<p class="fw-medium text-primary-light mb-1">Dalam Proses</p> <p class="fw-medium text-primary-light mb-1">Dalam Proses</p>
<h4 class="mb-1 fw-bold text-info-main">147</h4> <h4 class="mb-1 fw-bold text-info-main">
@php
$rintekProsesData = collect($rintekData)->firstWhere('id', 'proses');
@endphp
{{ number_format($rintekProsesData['value'] ?? 147) }}
</h4>
</div> </div>
<div class="w-50-px h-50-px bg-info rounded-circle d-flex justify-content-center align-items-center"> <div class="w-50-px h-50-px bg-info rounded-circle d-flex justify-content-center align-items-center">
@ -432,7 +494,12 @@
<div> <div>
<span class="badge text-bg-success">RINTEK</span> <span class="badge text-bg-success">RINTEK</span>
<p class="fw-medium text-primary-light mb-1">Izin Ditolak</p> <p class="fw-medium text-primary-light mb-1">Izin Ditolak</p>
<h4 class="mb-1 fw-bold text-danger-main">70</h4> <h4 class="mb-1 fw-bold text-danger-main">
@php
$rintekDitolakData = collect($rintekData)->firstWhere('id', 'ditolak');
@endphp
{{ number_format($rintekDitolakData['value'] ?? 70) }}
</h4>
</div> </div>
<div class="w-50-px h-50-px bg-red rounded-circle d-flex justify-content-center align-items-center"> <div class="w-50-px h-50-px bg-red rounded-circle d-flex justify-content-center align-items-center">
@ -641,7 +708,13 @@
<div> <div>
<span class="badge text-bg-info text-white">AMDAL</span> <span class="badge text-bg-info text-white">AMDAL</span>
<p class="fw-medium text-primary-light mb-1">Total Pengajuan</p> <p class="fw-medium text-primary-light mb-1">Total Pengajuan</p>
<h4 class="mb-1 fw-bold text-primary-600">456</h4> <h4 class="mb-1 fw-bold text-primary-600">
@php
$amdalData = $allStatistics['amdal']['data'] ?? [];
$amdalTotalData = collect($amdalData)->firstWhere('id', 'total');
@endphp
{{ number_format($amdalTotalData['value'] ?? 456) }}
</h4>
</div> </div>
<div class="w-50-px h-50-px bg-yellow rounded-circle d-flex justify-content-center align-items-center"> <div class="w-50-px h-50-px bg-yellow rounded-circle d-flex justify-content-center align-items-center">
<x-lucide-equal class="text-white w-32-px h-32-px mb-0"/> <x-lucide-equal class="text-white w-32-px h-32-px mb-0"/>
@ -659,7 +732,12 @@
<div> <div>
<span class="badge text-bg-info text-white">AMDAL</span> <span class="badge text-bg-info text-white">AMDAL</span>
<p class="fw-medium text-primary-light mb-1">Izin Disetujui</p> <p class="fw-medium text-primary-light mb-1">Izin Disetujui</p>
<h4 class="mb-1 fw-bold text-success-main">298</h4> <h4 class="mb-1 fw-bold text-success-main">
@php
$amdalSelesaiData = collect($amdalData)->firstWhere('id', 'selesai');
@endphp
{{ number_format($amdalSelesaiData['value'] ?? 298) }}
</h4>
</div> </div>
<div class="w-50-px h-50-px bg-green rounded-circle d-flex justify-content-center align-items-center"> <div class="w-50-px h-50-px bg-green rounded-circle d-flex justify-content-center align-items-center">
<x-lucide-circle-check class="text-white w-32-px h-32-px mb-0"/> <x-lucide-circle-check class="text-white w-32-px h-32-px mb-0"/>
@ -677,7 +755,12 @@
<div> <div>
<span class="badge text-bg-info text-white">AMDAL</span> <span class="badge text-bg-info text-white">AMDAL</span>
<p class="fw-medium text-primary-light mb-1">Dalam Proses</p> <p class="fw-medium text-primary-light mb-1">Dalam Proses</p>
<h4 class="mb-1 fw-bold text-info-main">112</h4> <h4 class="mb-1 fw-bold text-info-main">
@php
$amdalProsesData = collect($amdalData)->firstWhere('id', 'proses');
@endphp
{{ number_format($amdalProsesData['value'] ?? 112) }}
</h4>
</div> </div>
<div class="w-50-px h-50-px bg-info rounded-circle d-flex justify-content-center align-items-center"> <div class="w-50-px h-50-px bg-info rounded-circle d-flex justify-content-center align-items-center">
@ -696,7 +779,12 @@
<div> <div>
<span class="badge text-bg-info text-white">AMDAL</span> <span class="badge text-bg-info text-white">AMDAL</span>
<p class="fw-medium text-primary-light mb-1">Izin Ditolak</p> <p class="fw-medium text-primary-light mb-1">Izin Ditolak</p>
<h4 class="mb-1 fw-bold text-danger-main">46</h4> <h4 class="mb-1 fw-bold text-danger-main">
@php
$amdalDitolakData = collect($amdalData)->firstWhere('id', 'ditolak');
@endphp
{{ number_format($amdalDitolakData['value'] ?? 46) }}
</h4>
</div> </div>
<div class="w-50-px h-50-px bg-red rounded-circle d-flex justify-content-center align-items-center"> <div class="w-50-px h-50-px bg-red rounded-circle d-flex justify-content-center align-items-center">
@ -727,6 +815,18 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@php
$amdalFastestData = $allFastestData['amdal']['data'] ?? [];
@endphp
@forelse($amdalFastestData as $fastest)
<tr>
<td>{{ $fastest['rank_order'] }}</td>
<td class="text-sm">{{ $fastest['nama'] }}</td>
<td class="text-sm">{{ number_format($fastest['total']) }}</td>
<td class="text-sm d-none d-lg-table-cell">{{ $fastest['durasi_pemohon'] }}</td>
<td class="text-sm d-none d-lg-table-cell">{{ $fastest['durasi_petugas'] }}</td>
</tr>
@empty
<tr> <tr>
<td>1</td> <td>1</td>
<td class="text-sm">IZIN LINGKUNGAN KEGIATAN USAHA</td> <td class="text-sm">IZIN LINGKUNGAN KEGIATAN USAHA</td>
@ -762,6 +862,7 @@
<td class="text-sm d-none d-lg-table-cell">84 Jam 50 Menit</td> <td class="text-sm d-none d-lg-table-cell">84 Jam 50 Menit</td>
<td class="text-sm d-none d-lg-table-cell">58 Jam 25 Menit</td> <td class="text-sm d-none d-lg-table-cell">58 Jam 25 Menit</td>
</tr> </tr>
@endforelse
</tbody> </tbody>
</table> </table>
</div> </div>
@ -785,6 +886,17 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@php
$amdalTerakhirTerbitData = $allTerakhirTerbitData['amdal']['data'] ?? [];
@endphp
@forelse($amdalTerakhirTerbitData as $terakhir)
<tr>
<td>{{ $terakhir['rank_order'] }}</td>
<td class="text-sm">{{ $terakhir['nama_izin'] }}</td>
<td class="text-sm">{{ $terakhir['pemohon'] }}</td>
<td class="text-sm d-none d-lg-table-cell">{{ \App\Helpers\DashboardHelper::formatTanggalTerbit($terakhir['tanggal_terbit']) }}</td>
</tr>
@empty
<tr> <tr>
<td>1</td> <td>1</td>
<td class="text-sm">SERTIFIKAT LAIK OPERASI - PEMENUHAN BAKU MUTU EMISI</td> <td class="text-sm">SERTIFIKAT LAIK OPERASI - PEMENUHAN BAKU MUTU EMISI</td>
@ -815,6 +927,7 @@
<td class="text-sm">CV. BERKAH JAYA</td> <td class="text-sm">CV. BERKAH JAYA</td>
<td class="text-sm d-none d-lg-table-cell">11 Des 2024</td> <td class="text-sm d-none d-lg-table-cell">11 Des 2024</td>
</tr> </tr>
@endforelse
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -3,8 +3,16 @@
use Illuminate\Foundation\Console\ClosureCommand; use Illuminate\Foundation\Console\ClosureCommand;
use Illuminate\Foundation\Inspiring; use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
Artisan::command('inspire', function () { Artisan::command('inspire', function () {
/** @var ClosureCommand $this */ /** @var ClosureCommand $this */
$this->comment(Inspiring::quote()); $this->comment(Inspiring::quote());
})->purpose('Display an 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();

View File

@ -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

13
run-scheduler.sh 100644
View File

@ -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

View File

@ -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

15
sync-perizinan.bat 100644
View File

@ -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

25
sync-perizinan.sh 100644
View File

@ -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

View File

@ -0,0 +1,35 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
$app = require_once __DIR__ . '/bootstrap/app.php';
$app->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";
}

View File

View File

@ -0,0 +1,31 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
$app = require_once __DIR__ . '/bootstrap/app.php';
$app->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";
}