Compare commits
98 Commits
fix/bug-fa
...
e577da737b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e577da737b | ||
|
|
05ca927c38 | ||
|
|
fc4b419878 | ||
|
|
53d12d6798 | ||
|
|
809eb85255 | ||
|
|
8a513460bb | ||
|
|
fc74875cce | ||
|
|
beb7d935c9 | ||
|
|
5c4cebd2b3 | ||
|
|
cbe3d00c96 | ||
|
|
65d9247b46 | ||
|
|
63310f2748 | ||
|
|
c6257b79bf | ||
|
|
38493063c4 | ||
|
|
954b2d8716 | ||
|
|
41cfce589b | ||
|
|
8de1b51fea | ||
|
|
fef6ae7522 | ||
|
|
6f1cb4195a | ||
|
|
6a060f5dac | ||
|
|
844fbdfa89 | ||
|
|
e18c0cb3b6 | ||
|
|
0a9d9071e4 | ||
|
|
4b28bebcc2 | ||
|
|
1bcd2023da | ||
|
|
1b084ed485 | ||
|
|
71ca8dc553 | ||
|
|
3cddd271c8 | ||
|
|
e9a70a827c | ||
|
|
2cbc4172da | ||
|
|
d7e9f44b20 | ||
|
|
7c7aa0e2a5 | ||
|
|
0111ab14e1 | ||
|
|
68e9d5eebf | ||
|
|
209ef07f9c | ||
|
|
6896fd62a3 | ||
|
|
b73183becf | ||
|
|
9f9c3758ed | ||
|
|
48a340d684 | ||
|
|
3ff3dc8f17 | ||
|
|
7936eb1dbf | ||
|
|
ec047821a1 | ||
|
|
e0ed007a39 | ||
|
|
bb63ea8084 | ||
|
|
7a19d9f39d | ||
|
|
93af7ab2a1 | ||
|
|
6158903260 | ||
|
|
09e7d41ddc | ||
|
|
2f4ef6cb56 | ||
|
|
4f94e9d8f7 | ||
|
|
fa6a0079dc | ||
|
|
f7497cbec8 | ||
|
|
b5f7bf39b2 | ||
|
|
ef3c9d6fc3 | ||
|
|
1288ab509d | ||
|
|
588e3ad5e2 | ||
|
|
3902a486f7 | ||
|
|
dd1cd72450 | ||
|
|
af05a39a82 | ||
|
|
0abf278aa3 | ||
|
|
c2cb1b99f2 | ||
|
|
7135876ebc | ||
|
|
456eec83dc | ||
|
|
6a22b55a1c | ||
|
|
5aab6fa3d1 | ||
|
|
a1e302a56d | ||
|
|
a7f578ca3d | ||
|
|
c33193d5f0 | ||
|
|
2c7c99bcf1 | ||
|
|
a01b6f5611 | ||
|
|
2f43ebe97e | ||
|
|
e5baf5318f | ||
|
|
b895f61701 | ||
|
|
5dd92aa323 | ||
|
|
7eb5a850c2 | ||
|
|
200b398868 | ||
|
|
ccff82bd22 | ||
|
|
285e89d5d0 | ||
|
|
4c3443c2d6 | ||
|
|
df70a47bd1 | ||
|
|
e71dd7d213 | ||
|
|
f2eb998ac5 | ||
|
|
fc54e20fa4 | ||
|
|
6946fa7074 | ||
|
|
236b6f9bfc | ||
|
|
285ff46c2b | ||
|
|
a8b02afad9 | ||
|
|
a0666e78d2 | ||
|
|
799e409ce2 | ||
|
|
780ba60224 | ||
|
|
baed8cc487 | ||
|
|
e17f5beaf0 | ||
|
|
766e1a430c | ||
|
|
6677c320fc | ||
|
|
9437eb949f | ||
|
|
6f77120c33 | ||
|
|
f8d0573e5c | ||
|
|
ca74d0143f |
0
.editorconfig
Executable file → Normal file
0
.editorconfig
Executable file → Normal file
3
.env.example
Executable file → Normal file
3
.env.example
Executable file → Normal file
@@ -2,7 +2,7 @@ APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_TIMEZONE=UTC
|
||||
APP_TIMEZONE=Asia/Jakarta
|
||||
APP_URL=http://localhost
|
||||
API_URL=http://localhost:8000
|
||||
|
||||
@@ -65,6 +65,7 @@ AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
VITE_APP_HOST=localhost
|
||||
|
||||
API_KEY_GOOGLE="xxxxx"
|
||||
SPREAD_SHEET_ID="xxxxx"
|
||||
|
||||
0
.gitattributes
vendored
Executable file → Normal file
0
.gitattributes
vendored
Executable file → Normal file
3
.gitignore
vendored
Executable file → Normal file
3
.gitignore
vendored
Executable file → Normal file
@@ -21,3 +21,6 @@ yarn-error.log
|
||||
/.nova
|
||||
/.vscode
|
||||
/.zed
|
||||
/.composer
|
||||
/.config
|
||||
/.npm
|
||||
115
Dockerfile
Normal file
115
Dockerfile
Normal file
@@ -0,0 +1,115 @@
|
||||
FROM node:18 AS node-base
|
||||
|
||||
# Development stage
|
||||
FROM node-base AS development
|
||||
WORKDIR /var/www
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
EXPOSE 5173
|
||||
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
||||
|
||||
# Local development stage for PHP
|
||||
FROM php:8.2-fpm AS local
|
||||
WORKDIR /var/www
|
||||
|
||||
# Install PHP extensions
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git curl zip unzip libpng-dev libonig-dev libxml2-dev libzip-dev \
|
||||
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip
|
||||
|
||||
# Override PHP memory limit
|
||||
COPY docker/php/memory-limit.ini /usr/local/etc/php/conf.d/memory-limit.ini
|
||||
|
||||
# Install Node.js
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
|
||||
# Install Composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Create www-data user with same UID/GID as host user (1000:1000 is common for first user)
|
||||
RUN usermod -u 1000 www-data && groupmod -g 1000 www-data
|
||||
|
||||
# Copy application files
|
||||
COPY . .
|
||||
|
||||
# Install dependencies
|
||||
RUN composer install
|
||||
|
||||
# Create storage directories and set proper permissions
|
||||
RUN mkdir -p storage/framework/{sessions,views,cache} \
|
||||
&& mkdir -p storage/logs \
|
||||
&& mkdir -p bootstrap/cache \
|
||||
&& chown -R www-data:www-data /var/www \
|
||||
&& chmod -R 775 /var/www/storage \
|
||||
&& chmod -R 775 /var/www/bootstrap/cache
|
||||
|
||||
# Create entrypoint script to fix permissions on startup
|
||||
RUN echo '#!/bin/bash\n\
|
||||
chown -R www-data:www-data /var/www/storage /var/www/bootstrap/cache\n\
|
||||
chmod -R 775 /var/www/storage /var/www/bootstrap/cache\n\
|
||||
exec "$@"' > /entrypoint.sh && chmod +x /entrypoint.sh
|
||||
|
||||
USER www-data
|
||||
|
||||
EXPOSE 9000
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["php-fpm"]
|
||||
|
||||
# Production stage
|
||||
FROM php:8.2-fpm AS production
|
||||
WORKDIR /var/www
|
||||
|
||||
# Install PHP extensions
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git curl zip unzip libpng-dev libonig-dev libxml2-dev libzip-dev \
|
||||
supervisor \
|
||||
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip
|
||||
|
||||
# Override PHP memory limit
|
||||
COPY docker/php/memory-limit.ini /usr/local/etc/php/conf.d/memory-limit.ini
|
||||
|
||||
# Install Node.js
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
|
||||
# Install Composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Copy application files
|
||||
COPY . .
|
||||
|
||||
# Install dependencies
|
||||
RUN composer install --no-dev --optimize-autoloader
|
||||
|
||||
# Install and build frontend assets
|
||||
RUN npm install \
|
||||
&& npm run build \
|
||||
&& ls -la public/build \
|
||||
&& mkdir -p public/assets \
|
||||
&& cp -r public/build/* public/assets/ \
|
||||
&& ls -la public/assets \
|
||||
&& rm -rf node_modules \
|
||||
&& rm -rf public/build
|
||||
|
||||
# Laravel caches
|
||||
RUN php artisan config:clear \
|
||||
&& php artisan route:clear \
|
||||
&& php artisan view:clear \
|
||||
&& php artisan optimize
|
||||
|
||||
RUN php artisan storage:link
|
||||
|
||||
# Create supervisor directories
|
||||
RUN mkdir -p /var/log/supervisor /var/run/supervisor
|
||||
|
||||
# Copy supervisor configuration
|
||||
COPY docker/supervisor/supervisord.conf /etc/supervisor/supervisord.conf
|
||||
COPY docker/supervisor/laravel-production.conf /etc/supervisor/conf.d/laravel-production.conf
|
||||
|
||||
# Permissions
|
||||
RUN chown -R www-data:www-data /var/www && chmod -R 755 /var/www/storage /var/www/public
|
||||
|
||||
EXPOSE 9000
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||
191
README.md
Executable file → Normal file
191
README.md
Executable file → Normal file
@@ -1,112 +1,167 @@
|
||||
# Usage icon
|
||||
# Sibedas PBG Web
|
||||
|
||||
search or pick icon in <a href="https://icon-sets.iconify.design/mingcute/?keyword=mingcute">here</a>
|
||||
Aplikasi web untuk manajemen data PBG (Pendidikan Berkelanjutan Guru) dengan fitur integrasi Google Sheets.
|
||||
|
||||
# Set up queue for running automatically
|
||||
## 🚀 Quick Start
|
||||
|
||||
- Install Supervisor
|
||||
### Prerequisites
|
||||
|
||||
```
|
||||
sudo apt update && sudo apt install supervisor -y
|
||||
- Docker & Docker Compose
|
||||
- Domain name (untuk production)
|
||||
|
||||
### Local Development
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd sibedas-pbg-web
|
||||
./scripts/setup-local.sh
|
||||
# Access: http://localhost:8000
|
||||
```
|
||||
|
||||
- Create Supervisor Config
|
||||
### Production Deployment
|
||||
|
||||
```
|
||||
sudo nano /etc/supervisor/conf.d/laravel-worker.conf
|
||||
```bash
|
||||
# 1. Setup environment
|
||||
cp env.production.example .env
|
||||
nano .env
|
||||
|
||||
[program:laravel-worker]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=php /home/arifal/development/sibedas-pbg-web/artisan queue:work --queue=default --timeout=82800 --tries=1
|
||||
autostart=true
|
||||
autorestart=true
|
||||
numprocs=1
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/home/arifal/development/sibedas-pbg-web/storage/logs/worker.log
|
||||
stopasgroup=true
|
||||
killasgroup=true
|
||||
# 2. Deploy with SSL (Recommended)
|
||||
./scripts/setup-reverse-proxy.sh setup
|
||||
|
||||
# 3. Check status
|
||||
./scripts/setup-reverse-proxy.sh status
|
||||
# Access: https://yourdomain.com
|
||||
```
|
||||
|
||||
- Reload Supervisor
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Local Development
|
||||
|
||||
```
|
||||
sudo supervisorctl reread
|
||||
sudo supervisorctl update
|
||||
sudo supervisorctl start laravel-worker
|
||||
sudo supervisorctl restart laravel-worker
|
||||
sudo supervisorctl status
|
||||
Browser → Port 8000 → Nginx → PHP-FPM → MariaDB
|
||||
```
|
||||
|
||||
# How to running
|
||||
|
||||
- Install composer package
|
||||
### Production dengan Reverse Proxy
|
||||
|
||||
```
|
||||
composer install
|
||||
Internet → Reverse Proxy (80/443) → Internal Nginx → PHP-FPM → MariaDB
|
||||
```
|
||||
|
||||
- Install npm package
|
||||
## 🔧 Configuration
|
||||
|
||||
```
|
||||
npm install && npm run build
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Domain & SSL
|
||||
DOMAIN=sibedas.yourdomain.com
|
||||
EMAIL=admin@yourdomain.com
|
||||
SSL_TYPE=self-signed # atau letsencrypt
|
||||
|
||||
# Database
|
||||
DB_PASSWORD=your_secure_password
|
||||
MYSQL_ROOT_PASSWORD=your_root_password
|
||||
|
||||
# Laravel
|
||||
APP_KEY=base64:your_app_key_here
|
||||
APP_URL=https://sibedas.yourdomain.com
|
||||
```
|
||||
|
||||
- Create symlinks storage
|
||||
## 🚀 Production Deployment Steps
|
||||
|
||||
```
|
||||
php artisan storage:link
|
||||
### 1. Server Preparation
|
||||
|
||||
```bash
|
||||
# Install Docker & Docker Compose
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sudo sh get-docker.sh
|
||||
sudo usermod -aG docker $USER
|
||||
|
||||
# Install Docker Compose
|
||||
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
```
|
||||
|
||||
- Running migration
|
||||
### 2. Clone & Setup
|
||||
|
||||
```
|
||||
php artisan migrate
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd sibedas-pbg-web
|
||||
chmod +x scripts/*.sh
|
||||
cp env.production.example .env
|
||||
nano .env
|
||||
```
|
||||
|
||||
- Running seeder
|
||||
### 3. Deploy
|
||||
|
||||
```
|
||||
php artisan db:seed
|
||||
```bash
|
||||
# Full deployment with SSL
|
||||
./scripts/setup-reverse-proxy.sh setup
|
||||
|
||||
# Or step by step
|
||||
./scripts/deploy-production.sh deploy
|
||||
./scripts/setup-ssl.sh letsencrypt
|
||||
```
|
||||
|
||||
- Create view table
|
||||
- excute all sql queries on folder database/view_query
|
||||
### 4. Verify
|
||||
|
||||
# Add ENV variable
|
||||
|
||||
- API_KEY_GOOGLE
|
||||
|
||||
```
|
||||
Get api key from google developer console for and turn on spreadsheet api or feaature for google sheet
|
||||
```bash
|
||||
docker-compose ps
|
||||
./scripts/setup-reverse-proxy.sh status
|
||||
curl -f http://localhost/health-check
|
||||
```
|
||||
|
||||
- SPREAD_SHEET_ID
|
||||
## 📊 Monitoring
|
||||
|
||||
```
|
||||
Get spreadsheet id from google sheet link
|
||||
```bash
|
||||
# Check status
|
||||
./scripts/setup-reverse-proxy.sh status
|
||||
|
||||
# View logs
|
||||
docker-compose logs [service]
|
||||
|
||||
# Check SSL certificate
|
||||
./scripts/setup-ssl.sh check
|
||||
```
|
||||
|
||||
- OPENAI_API_KEY
|
||||
## 🛠️ Common Commands
|
||||
|
||||
```
|
||||
Get OpenAI API key from chatgpt subscription
|
||||
```bash
|
||||
# Start services
|
||||
docker-compose up -d
|
||||
|
||||
# Stop services
|
||||
docker-compose down
|
||||
|
||||
# Restart services
|
||||
docker-compose restart
|
||||
|
||||
# Execute Laravel commands
|
||||
docker-compose exec app php artisan [command]
|
||||
|
||||
# Backup database
|
||||
docker exec sibedas_db mysqldump -u root -p sibedas > backup.sql
|
||||
```
|
||||
|
||||
- ENV
|
||||
## 📁 Scripts
|
||||
|
||||
```
|
||||
### Essential Scripts
|
||||
|
||||
API_KEY_GOOGLE="xxxxx"
|
||||
SPREAD_SHEET_ID="xxxxx"
|
||||
OPENAI_API_KEY="xxxxx"
|
||||
- `scripts/setup-reverse-proxy.sh` - Setup lengkap reverse proxy dan SSL
|
||||
- `scripts/deploy-production.sh` - Deployment production
|
||||
- `scripts/setup-ssl.sh` - Setup SSL certificates
|
||||
|
||||
```
|
||||
### Optional Scripts
|
||||
|
||||
# Technology version
|
||||
- `scripts/setup-local.sh` - Setup local development
|
||||
- `scripts/import-sibedas-database.sh` - Manual database import
|
||||
|
||||
- php 8.3
|
||||
- Laravel 11
|
||||
- node v22.13.0
|
||||
- npm 10.9.2
|
||||
- mariadb Ver 15.1 Distrib 10.6.18-MariaDB, for debian-linux-gnu (x86_64) using EditLine wrapper
|
||||
- Ubuntu 24.04
|
||||
## 📚 Documentation
|
||||
|
||||
Untuk dokumentasi lengkap, lihat [docs/README.md](docs/README.md)
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
1. Check logs: `docker-compose logs [service]`
|
||||
2. Check status: `./scripts/setup-reverse-proxy.sh status`
|
||||
3. Restart services: `docker-compose restart`
|
||||
4. Review documentation di folder `docs/`
|
||||
|
||||
482
app/Console/Commands/AssignSpatialPlanningsToCalculation.php
Normal file
482
app/Console/Commands/AssignSpatialPlanningsToCalculation.php
Normal file
@@ -0,0 +1,482 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\SpatialPlanning;
|
||||
use App\Models\RetributionCalculation;
|
||||
use App\Models\BuildingType;
|
||||
use App\Services\RetributionCalculatorService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AssignSpatialPlanningsToCalculation extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'spatial-planning:assign-calculations
|
||||
{--force : Force assign even if already has calculation}
|
||||
{--recalculate : Recalculate existing calculations with new values}
|
||||
{--chunk=100 : Process in chunks}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Assign retribution calculations to spatial plannings (recalculate mode recalculates with current values)';
|
||||
|
||||
protected $calculatorService;
|
||||
|
||||
public function __construct(RetributionCalculatorService $calculatorService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->calculatorService = $calculatorService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('🏗️ Starting spatial planning calculation assignment...');
|
||||
|
||||
// Get processing options
|
||||
$force = $this->option('force');
|
||||
$recalculate = $this->option('recalculate');
|
||||
$chunkSize = (int) $this->option('chunk');
|
||||
|
||||
// Get spatial plannings query
|
||||
$query = SpatialPlanning::query();
|
||||
|
||||
if ($recalculate) {
|
||||
// Recalculate mode: only process those WITH active calculations
|
||||
$query->whereHas('retributionCalculations', function ($q) {
|
||||
$q->where('is_active', true);
|
||||
});
|
||||
$this->info('🔄 Recalculate mode: Processing spatial plannings with existing calculations');
|
||||
$this->warn('⚠️ NOTE: Recalculate mode will recalculate all existing calculations with current values');
|
||||
} elseif (!$force) {
|
||||
// Normal mode: only process those without active calculations
|
||||
$query->whereDoesntHave('retributionCalculations', function ($q) {
|
||||
$q->where('is_active', true);
|
||||
});
|
||||
$this->info('➕ Normal mode: Processing spatial plannings without calculations');
|
||||
} else {
|
||||
// Force mode: process all
|
||||
$this->info('🔥 Force mode: Processing ALL spatial plannings');
|
||||
}
|
||||
|
||||
$totalRecords = $query->count();
|
||||
|
||||
if ($totalRecords === 0) {
|
||||
$this->warn('No spatial plannings found to process.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->info("Found {$totalRecords} spatial planning(s) to process");
|
||||
|
||||
if (!$this->confirm('Do you want to continue?')) {
|
||||
$this->info('Operation cancelled.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Process in chunks
|
||||
$processed = 0;
|
||||
$errors = 0;
|
||||
$reused = 0;
|
||||
$created = 0;
|
||||
$buildingTypeStats = [];
|
||||
|
||||
$progressBar = $this->output->createProgressBar($totalRecords);
|
||||
$progressBar->start();
|
||||
|
||||
$recalculated = 0;
|
||||
|
||||
$query->chunk($chunkSize, function ($spatialPlannings) use (&$processed, &$errors, &$reused, &$created, &$recalculated, &$buildingTypeStats, $progressBar, $recalculate) {
|
||||
foreach ($spatialPlannings as $spatialPlanning) {
|
||||
try {
|
||||
$result = $this->assignCalculationToSpatialPlanning($spatialPlanning, $recalculate);
|
||||
|
||||
if ($result['reused']) {
|
||||
$reused++;
|
||||
} elseif (isset($result['recalculated']) && $result['recalculated']) {
|
||||
$recalculated++;
|
||||
} else {
|
||||
$created++;
|
||||
}
|
||||
|
||||
// Track building type statistics
|
||||
$buildingTypeName = $result['building_type_name'] ?? 'Unknown';
|
||||
if (!isset($buildingTypeStats[$buildingTypeName])) {
|
||||
$buildingTypeStats[$buildingTypeName] = 0;
|
||||
}
|
||||
$buildingTypeStats[$buildingTypeName]++;
|
||||
|
||||
$processed++;
|
||||
} catch (\Exception $e) {
|
||||
$errors++;
|
||||
$this->error("Error processing ID {$spatialPlanning->id}: " . $e->getMessage());
|
||||
}
|
||||
|
||||
$progressBar->advance();
|
||||
}
|
||||
});
|
||||
|
||||
$progressBar->finish();
|
||||
|
||||
// Show summary
|
||||
$this->newLine(2);
|
||||
$this->info('✅ Assignment completed!');
|
||||
|
||||
if ($recalculate) {
|
||||
$this->table(
|
||||
['Metric', 'Count'],
|
||||
[
|
||||
['Total Processed', $processed],
|
||||
['Recalculated (Changed)', $recalculated],
|
||||
['Unchanged', $reused],
|
||||
['Errors', $errors],
|
||||
]
|
||||
);
|
||||
$this->info('📊 Recalculate mode recalculated all existing calculations with current values');
|
||||
} else {
|
||||
$this->table(
|
||||
['Metric', 'Count'],
|
||||
[
|
||||
['Total Processed', $processed],
|
||||
['Calculations Created', $created],
|
||||
['Calculations Reused', $reused],
|
||||
['Errors', $errors],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Show building type statistics
|
||||
if (!empty($buildingTypeStats)) {
|
||||
$this->newLine();
|
||||
$this->info('📊 Building Type Distribution:');
|
||||
$statsRows = [];
|
||||
arsort($buildingTypeStats); // Sort by count descending
|
||||
foreach ($buildingTypeStats as $typeName => $count) {
|
||||
$percentage = round(($count / $processed) * 100, 1);
|
||||
$statsRows[] = [$typeName, $count, $percentage . '%'];
|
||||
}
|
||||
$this->table(['Building Type', 'Count', 'Percentage'], $statsRows);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign calculation to a spatial planning
|
||||
*/
|
||||
private function assignCalculationToSpatialPlanning(SpatialPlanning $spatialPlanning, bool $recalculate = false): array
|
||||
{
|
||||
// 1. Detect building type
|
||||
$buildingType = $this->detectBuildingType($spatialPlanning->building_function);
|
||||
|
||||
// 2. Get calculation parameters (round to 2 decimal places)
|
||||
$floorNumber = $spatialPlanning->number_of_floors ?: 1;
|
||||
$buildingArea = round($spatialPlanning->getCalculationArea(), 2);
|
||||
|
||||
if ($buildingArea <= 0) {
|
||||
throw new \Exception("Invalid building area: {$buildingArea}");
|
||||
}
|
||||
|
||||
$reused = false;
|
||||
$isRecalculated = false;
|
||||
|
||||
if ($recalculate) {
|
||||
// Recalculate mode: Always create new calculation
|
||||
$calculationResult = $this->performCalculation($spatialPlanning, $buildingType, true);
|
||||
|
||||
// Check if spatial planning has existing active calculation
|
||||
$currentActiveCalculation = $spatialPlanning->activeRetributionCalculation;
|
||||
|
||||
if ($currentActiveCalculation) {
|
||||
$oldAmount = $currentActiveCalculation->retributionCalculation->retribution_amount;
|
||||
$oldArea = $currentActiveCalculation->retributionCalculation->building_area;
|
||||
$newAmount = $calculationResult['amount'];
|
||||
|
||||
// Check if there's a significant difference (more than 1 rupiah)
|
||||
if (abs($oldAmount - $newAmount) > 1) {
|
||||
// Create new calculation
|
||||
$calculation = RetributionCalculation::create([
|
||||
'building_type_id' => $buildingType->id,
|
||||
'floor_number' => $floorNumber,
|
||||
'building_area' => $buildingArea,
|
||||
'retribution_amount' => $calculationResult['amount'],
|
||||
'calculation_detail' => $calculationResult['detail'],
|
||||
]);
|
||||
|
||||
// Assign new calculation
|
||||
$spatialPlanning->assignRetributionCalculation(
|
||||
$calculation,
|
||||
"Recalculated: Original area {$oldArea}m² → New area {$buildingArea}m², Amount {$oldAmount}→{$newAmount}"
|
||||
);
|
||||
|
||||
$isRecalculated = true;
|
||||
} else {
|
||||
// No significant difference, keep existing
|
||||
$calculation = $currentActiveCalculation->retributionCalculation;
|
||||
$reused = true;
|
||||
}
|
||||
} else {
|
||||
// No existing calculation, create new
|
||||
$calculation = RetributionCalculation::create([
|
||||
'building_type_id' => $buildingType->id,
|
||||
'floor_number' => $floorNumber,
|
||||
'building_area' => $buildingArea,
|
||||
'retribution_amount' => $calculationResult['amount'],
|
||||
'calculation_detail' => $calculationResult['detail'],
|
||||
]);
|
||||
|
||||
$spatialPlanning->assignRetributionCalculation(
|
||||
$calculation,
|
||||
'Recalculated (new calculation with current values)'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Normal mode: Check if calculation already exists with same parameters
|
||||
$existingCalculation = RetributionCalculation::where([
|
||||
'building_type_id' => $buildingType->id,
|
||||
'floor_number' => $floorNumber,
|
||||
])
|
||||
->whereBetween('building_area', [
|
||||
$buildingArea * 0.99, // 1% tolerance
|
||||
$buildingArea * 1.01
|
||||
])
|
||||
->first();
|
||||
|
||||
if ($existingCalculation) {
|
||||
// Reuse existing calculation
|
||||
$calculation = $existingCalculation;
|
||||
$reused = true;
|
||||
} else {
|
||||
// Create new calculation
|
||||
$calculationResult = $this->performCalculation($spatialPlanning, $buildingType, false);
|
||||
|
||||
$calculation = RetributionCalculation::create([
|
||||
'building_type_id' => $buildingType->id,
|
||||
'floor_number' => $floorNumber,
|
||||
'building_area' => $buildingArea,
|
||||
'retribution_amount' => $calculationResult['amount'],
|
||||
'calculation_detail' => $calculationResult['detail'],
|
||||
]);
|
||||
}
|
||||
|
||||
// Assign to spatial planning
|
||||
$spatialPlanning->assignRetributionCalculation(
|
||||
$calculation,
|
||||
$reused ? 'Auto-assigned (reused calculation)' : 'Auto-assigned (new calculation)'
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'calculation' => $calculation,
|
||||
'reused' => $reused,
|
||||
'recalculated' => $isRecalculated,
|
||||
'building_type_name' => $buildingType->name,
|
||||
'building_type_code' => $buildingType->code,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect building type based on building function using database
|
||||
*/
|
||||
private function detectBuildingType(string $buildingFunction = null): BuildingType
|
||||
{
|
||||
$function = strtolower($buildingFunction ?? '');
|
||||
|
||||
// Mapping building functions to building type codes from database
|
||||
$mappings = [
|
||||
// Religious
|
||||
'masjid' => 'KEAGAMAAN',
|
||||
'gereja' => 'KEAGAMAAN',
|
||||
'vihara' => 'KEAGAMAAN',
|
||||
'pura' => 'KEAGAMAAN',
|
||||
'keagamaan' => 'KEAGAMAAN',
|
||||
'religious' => 'KEAGAMAAN',
|
||||
|
||||
// Residential/Housing
|
||||
'rumah' => 'HUN_SEDH', // Default to simple housing
|
||||
'perumahan' => 'HUN_SEDH',
|
||||
'hunian' => 'HUN_SEDH',
|
||||
'residential' => 'HUN_SEDH',
|
||||
'tinggal' => 'HUN_SEDH',
|
||||
'mbr' => 'MBR', // Specifically for MBR
|
||||
'masyarakat berpenghasilan rendah' => 'MBR',
|
||||
|
||||
// Commercial/Business - default to UMKM
|
||||
'toko' => 'UMKM',
|
||||
'warung' => 'UMKM',
|
||||
'perdagangan' => 'UMKM',
|
||||
'dagang' => 'UMKM',
|
||||
'usaha' => 'UMKM',
|
||||
'komersial' => 'UMKM',
|
||||
'commercial' => 'UMKM',
|
||||
'pasar' => 'UMKM',
|
||||
'kios' => 'UMKM',
|
||||
|
||||
// Large commercial
|
||||
'mall' => 'USH_BESAR',
|
||||
'plaza' => 'USH_BESAR',
|
||||
'supermarket' => 'USH_BESAR',
|
||||
'department' => 'USH_BESAR',
|
||||
'hotel' => 'USH_BESAR',
|
||||
'resort' => 'USH_BESAR',
|
||||
|
||||
// Office
|
||||
'kantor' => 'UMKM', // Can be UMKM or USH_BESAR depending on size
|
||||
'perkantoran' => 'UMKM',
|
||||
'office' => 'UMKM',
|
||||
|
||||
// Industry (usually big business)
|
||||
'industri' => 'USH_BESAR',
|
||||
'pabrik' => 'USH_BESAR',
|
||||
'gudang' => 'USH_BESAR',
|
||||
'warehouse' => 'USH_BESAR',
|
||||
'manufacturing' => 'USH_BESAR',
|
||||
|
||||
// Social/Cultural
|
||||
'sekolah' => 'SOSBUDAYA',
|
||||
'pendidikan' => 'SOSBUDAYA',
|
||||
'universitas' => 'SOSBUDAYA',
|
||||
'kampus' => 'SOSBUDAYA',
|
||||
'rumah sakit' => 'SOSBUDAYA',
|
||||
'klinik' => 'SOSBUDAYA',
|
||||
'kesehatan' => 'SOSBUDAYA',
|
||||
'puskesmas' => 'SOSBUDAYA',
|
||||
'museum' => 'SOSBUDAYA',
|
||||
'perpustakaan' => 'SOSBUDAYA',
|
||||
'gedung olahraga' => 'SOSBUDAYA',
|
||||
|
||||
// Mixed use
|
||||
'campuran' => 'CAMP_KECIL', // Default to small mixed
|
||||
'mixed' => 'CAMP_KECIL',
|
||||
];
|
||||
|
||||
// Try to match building function
|
||||
$detectedCode = null;
|
||||
foreach ($mappings as $keyword => $code) {
|
||||
if (str_contains($function, $keyword)) {
|
||||
$detectedCode = $code;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Find building type in database by code
|
||||
if ($detectedCode) {
|
||||
$buildingType = BuildingType::where('code', $detectedCode)
|
||||
->whereHas('indices') // Only types with indices
|
||||
->first();
|
||||
|
||||
if ($buildingType) {
|
||||
return $buildingType;
|
||||
}
|
||||
}
|
||||
|
||||
// Default to "UMKM" type if not detected (most common business type)
|
||||
$defaultType = BuildingType::where('code', 'UMKM')
|
||||
->whereHas('indices')
|
||||
->first();
|
||||
|
||||
if ($defaultType) {
|
||||
return $defaultType;
|
||||
}
|
||||
|
||||
// Fallback to any available type with indices
|
||||
$fallbackType = BuildingType::whereHas('indices')
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
if (!$fallbackType) {
|
||||
throw new \Exception('No building types with indices found in database. Please run: php artisan db:seed --class=RetributionDataSeeder');
|
||||
}
|
||||
|
||||
return $fallbackType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform calculation using RetributionCalculatorService
|
||||
*/
|
||||
private function performCalculation(SpatialPlanning $spatialPlanning, BuildingType $buildingType, bool $recalculate = false): array
|
||||
{
|
||||
// Round area to 2 decimal places to match database storage format
|
||||
$buildingArea = round($spatialPlanning->getCalculationArea(), 2);
|
||||
|
||||
// For recalculate mode, use the current area without any adjustment
|
||||
if ($recalculate) {
|
||||
$this->info("Recalculate mode: Using current area {$buildingArea}m²");
|
||||
}
|
||||
|
||||
$floorNumber = $spatialPlanning->number_of_floors ?: 1;
|
||||
|
||||
try {
|
||||
// Use the same calculation service as TestRetributionCalculation
|
||||
$result = $this->calculatorService->calculate(
|
||||
$buildingType->id,
|
||||
$floorNumber,
|
||||
$buildingArea,
|
||||
false // Don't save to database, we'll handle that separately
|
||||
);
|
||||
|
||||
return [
|
||||
'amount' => $result['total_retribution'],
|
||||
'detail' => [
|
||||
'building_type_id' => $buildingType->id,
|
||||
'building_type_name' => $buildingType->name,
|
||||
'building_type_code' => $buildingType->code,
|
||||
'coefficient' => $result['indices']['coefficient'],
|
||||
'ip_permanent' => $result['indices']['ip_permanent'],
|
||||
'ip_complexity' => $result['indices']['ip_complexity'],
|
||||
'locality_index' => $result['indices']['locality_index'],
|
||||
'height_index' => $result['input_parameters']['height_index'],
|
||||
'infrastructure_factor' => $result['indices']['infrastructure_factor'],
|
||||
'building_area' => $buildingArea,
|
||||
'floor_number' => $floorNumber,
|
||||
'building_function' => $spatialPlanning->building_function,
|
||||
'calculation_steps' => $result['calculation_detail'],
|
||||
'base_value' => $result['input_parameters']['base_value'],
|
||||
'is_free' => $buildingType->is_free,
|
||||
'calculation_date' => now()->toDateTimeString(),
|
||||
'total' => $result['total_retribution'],
|
||||
'is_recalculated' => $recalculate,
|
||||
]
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// Fallback to basic calculation if service fails
|
||||
$this->warn("Calculation service failed for {$spatialPlanning->name}: {$e->getMessage()}. Using fallback calculation.");
|
||||
|
||||
// Basic fallback calculation
|
||||
$totalAmount = $buildingType->is_free ? 0 : ($buildingArea * 50000);
|
||||
|
||||
// For recalculate mode in fallback, use current amount without adjustment
|
||||
if ($recalculate) {
|
||||
$this->warn("Fallback recalculate: Using current amount Rp{$totalAmount}");
|
||||
}
|
||||
|
||||
return [
|
||||
'amount' => $totalAmount,
|
||||
'detail' => [
|
||||
'building_type_id' => $buildingType->id,
|
||||
'building_type_name' => $buildingType->name,
|
||||
'building_type_code' => $buildingType->code,
|
||||
'building_area' => $buildingArea,
|
||||
'floor_number' => $floorNumber,
|
||||
'building_function' => $spatialPlanning->building_function,
|
||||
'calculation_method' => 'fallback',
|
||||
'error_message' => $e->getMessage(),
|
||||
'is_free' => $buildingType->is_free,
|
||||
'calculation_date' => now()->toDateTimeString(),
|
||||
'total' => $totalAmount,
|
||||
'is_recalculated' => $recalculate,
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\SyncronizeSIMBG;
|
||||
use App\Services\ServiceSIMBG;
|
||||
use Illuminate\Console\Command;
|
||||
use \Illuminate\Support\Facades\Log;
|
||||
|
||||
class ExecuteScraping extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:execute-scraping';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Execure scraping service daily every 12 pm';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
|
||||
private $service_simbg;
|
||||
|
||||
public function __construct(){
|
||||
parent::__construct();
|
||||
}
|
||||
public function handle()
|
||||
{
|
||||
SyncronizeSIMBG::dispatch()->onQueue('default');
|
||||
Log::info("running scheduler daily scraping");
|
||||
}
|
||||
}
|
||||
790
app/Console/Commands/InjectSpatialPlanningsData.php
Normal file
790
app/Console/Commands/InjectSpatialPlanningsData.php
Normal file
@@ -0,0 +1,790 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\SpatialPlanning;
|
||||
use Illuminate\Console\Command;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Exception;
|
||||
|
||||
class InjectSpatialPlanningsData extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'spatial-planning:inject
|
||||
{--file=storage/app/public/templates/2025.xlsx : Path to Excel file}
|
||||
{--sheet=0 : Sheet index to read from}
|
||||
{--dry-run : Run without actually inserting data}
|
||||
{--debug : Show Excel content for debugging}
|
||||
{--truncate : Clear existing data before import}
|
||||
{--no-truncate : Skip truncation (keep existing data)}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Inject spatial planning data from Excel file with BCR area calculation';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
$filePath = $this->option('file');
|
||||
$sheetIndex = (int) $this->option('sheet');
|
||||
$isDryRun = $this->option('dry-run');
|
||||
$isDebug = $this->option('debug');
|
||||
$shouldTruncate = $this->option('truncate');
|
||||
$noTruncate = $this->option('no-truncate');
|
||||
|
||||
if (!file_exists($filePath)) {
|
||||
$this->error("File not found: {$filePath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->info("Reading Excel file: {$filePath}");
|
||||
$this->info("Sheet index: {$sheetIndex}");
|
||||
|
||||
if ($isDryRun) {
|
||||
$this->warn("DRY RUN MODE - No data will be inserted");
|
||||
}
|
||||
|
||||
// Check existing data
|
||||
$existingCount = DB::table('spatial_plannings')->count();
|
||||
if ($existingCount > 0) {
|
||||
$this->info("Found {$existingCount} existing spatial planning records");
|
||||
} else {
|
||||
$this->info('No existing spatial planning data found');
|
||||
}
|
||||
|
||||
// Handle truncation logic
|
||||
$willTruncate = false;
|
||||
|
||||
if ($shouldTruncate) {
|
||||
$willTruncate = true;
|
||||
$this->info('Truncation requested via --truncate option');
|
||||
} elseif ($noTruncate) {
|
||||
$willTruncate = false;
|
||||
$this->info('Truncation skipped via --no-truncate option');
|
||||
} else {
|
||||
// Default behavior: ask user if not in dry run mode
|
||||
if (!$isDryRun) {
|
||||
$willTruncate = $this->confirm('Do you want to clear existing spatial planning data before import?');
|
||||
} else {
|
||||
$willTruncate = false;
|
||||
$this->info('DRY RUN MODE - Truncation will be skipped');
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm truncation if not in dry run mode and truncation is requested
|
||||
if ($willTruncate && !$isDryRun) {
|
||||
if (!$this->confirm('This will delete all existing spatial planning data and related retribution calculations. Continue?')) {
|
||||
$this->info('Operation cancelled.');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Truncate all related data properly
|
||||
if ($willTruncate && !$isDryRun) {
|
||||
$this->info('Truncating spatial planning data and related retribution calculations...');
|
||||
|
||||
try {
|
||||
// Disable foreign key checks for safe truncation
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
|
||||
|
||||
// 1. Delete calculable retributions for spatial plannings (polymorphic relationship)
|
||||
$deletedCalculableRetributions = DB::table('calculable_retributions')
|
||||
->where('calculable_type', 'App\\Models\\SpatialPlanning')
|
||||
->count();
|
||||
|
||||
if ($deletedCalculableRetributions > 0) {
|
||||
DB::table('calculable_retributions')
|
||||
->where('calculable_type', 'App\\Models\\SpatialPlanning')
|
||||
->delete();
|
||||
$this->info("Deleted {$deletedCalculableRetributions} calculable retributions for spatial plannings.");
|
||||
}
|
||||
|
||||
// 2. Truncate spatial plannings table
|
||||
DB::table('spatial_plannings')->truncate();
|
||||
$this->info('Spatial plannings table truncated successfully.');
|
||||
|
||||
// Re-enable foreign key checks
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
|
||||
|
||||
$this->info('All spatial planning data and related retribution calculations cleared successfully.');
|
||||
|
||||
} catch (Exception $e) {
|
||||
// Make sure to re-enable foreign key checks even on error
|
||||
try {
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
|
||||
} catch (Exception $fkError) {
|
||||
$this->error('Failed to re-enable foreign key checks: ' . $fkError->getMessage());
|
||||
}
|
||||
|
||||
$this->error('Failed to truncate spatial planning data: ' . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
} elseif ($willTruncate && $isDryRun) {
|
||||
$this->info('DRY RUN MODE - Would truncate spatial planning data and related retribution calculations');
|
||||
} else {
|
||||
$this->info('Keeping existing data (no truncation)');
|
||||
}
|
||||
|
||||
$spreadsheet = IOFactory::load($filePath);
|
||||
$worksheet = $spreadsheet->getSheet($sheetIndex);
|
||||
$rows = $worksheet->toArray(null, true, true, true);
|
||||
|
||||
if ($isDebug) {
|
||||
$this->info("=== EXCEL CONTENT DEBUG ===");
|
||||
foreach (array_slice($rows, 0, 20) as $index => $row) {
|
||||
if (!empty(array_filter($row))) {
|
||||
$this->line("Row $index: " . json_encode($row));
|
||||
}
|
||||
}
|
||||
$this->info("=== END DEBUG ===");
|
||||
}
|
||||
|
||||
// Find BCR percentages from last rows (columns D and E)
|
||||
$bcrPercentages = $this->findBcrPercentages($rows);
|
||||
$this->info("Found BCR Percentages: " . json_encode($bcrPercentages));
|
||||
|
||||
// Process data by sections
|
||||
$sections = $this->processSections($rows, $bcrPercentages, $isDebug);
|
||||
|
||||
$this->info("Found " . count($sections) . " sections");
|
||||
|
||||
$totalInserted = 0;
|
||||
foreach ($sections as $sectionIndex => $section) {
|
||||
$this->info("Processing Section " . ($sectionIndex + 1) . ": " . $section['applicant_name']);
|
||||
|
||||
// Gudang/pergudangan keywords successfully added to Fungsi Usaha classification
|
||||
|
||||
if (!$isDryRun) {
|
||||
$inserted = $this->insertSpatialPlanningData($section);
|
||||
$totalInserted += $inserted;
|
||||
$this->info("Inserted {$inserted} record for this section");
|
||||
} else {
|
||||
$this->info("Would insert 1 record for this section");
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isDryRun) {
|
||||
$this->info("Successfully inserted {$totalInserted} spatial planning records");
|
||||
|
||||
// Show summary of what was done
|
||||
$finalCount = DB::table('spatial_plannings')->count();
|
||||
$this->info("Final spatial planning records count: {$finalCount}");
|
||||
|
||||
if ($willTruncate) {
|
||||
$this->info("✅ Data import completed with truncation");
|
||||
} else {
|
||||
$this->info("✅ Data import completed (existing data preserved)");
|
||||
}
|
||||
} else {
|
||||
$this->info("Dry run completed. Total records that would be inserted: " . count($sections));
|
||||
if ($willTruncate) {
|
||||
$this->info("Would truncate existing data before import");
|
||||
} else {
|
||||
$this->info("Would preserve existing data during import");
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
} catch (Exception $e) {
|
||||
$this->error("Error: " . $e->getMessage());
|
||||
Log::error("InjectSpatialPlanningsData failed", ['error' => $e->getMessage()]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find BCR percentages from last rows in columns D and E
|
||||
*/
|
||||
private function findBcrPercentages(array $rows): array
|
||||
{
|
||||
$bcrPercentages = [];
|
||||
|
||||
// Look for BCR percentages in the last few rows
|
||||
$totalRows = count($rows);
|
||||
$searchRows = max(1, $totalRows - 10); // Search last 10 rows
|
||||
|
||||
for ($i = $totalRows; $i >= $searchRows; $i--) {
|
||||
if (isset($rows[$i]['D']) && isset($rows[$i]['E'])) {
|
||||
$valueD = $this->cleanNumericValue($rows[$i]['D']);
|
||||
$valueE = $this->cleanNumericValue($rows[$i]['E']);
|
||||
|
||||
// Check if these look like percentages (between 0 and 100)
|
||||
if ($valueD > 0 && $valueD <= 100 && $valueE > 0 && $valueE <= 100) {
|
||||
$bcrPercentages['D'] = $valueD;
|
||||
$bcrPercentages['E'] = $valueE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default values if not found
|
||||
if (empty($bcrPercentages)) {
|
||||
$bcrPercentages = ['D' => 60, 'E' => 40]; // Default BCR percentages
|
||||
}
|
||||
|
||||
return $bcrPercentages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process data by sections (each applicant)
|
||||
*/
|
||||
private function processSections(array $rows, array $bcrPercentages, bool $isDebug): array
|
||||
{
|
||||
$sections = [];
|
||||
$currentSection = null;
|
||||
$currentSectionNumber = null;
|
||||
$sectionData = [];
|
||||
|
||||
foreach ($rows as $rowIndex => $row) {
|
||||
// Skip empty rows
|
||||
if (empty(array_filter($row))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($isDebug) {
|
||||
$this->line("Checking row $rowIndex: " . substr(json_encode($row), 0, 100) . "...");
|
||||
}
|
||||
|
||||
// Check if this is a new section (applicant)
|
||||
if ($this->isNewSection($row)) {
|
||||
if ($isDebug) {
|
||||
$this->info("Found new section at row $rowIndex");
|
||||
}
|
||||
|
||||
// Save previous section if exists
|
||||
if ($currentSection && !empty($sectionData)) {
|
||||
$sections[] = [
|
||||
'applicant_name' => $currentSection,
|
||||
'section_number' => $currentSectionNumber,
|
||||
'data' => $sectionData
|
||||
];
|
||||
if ($isDebug) {
|
||||
$this->info("Saved section: $currentSection with " . count($sectionData) . " data rows");
|
||||
}
|
||||
}
|
||||
|
||||
// Start new section
|
||||
$currentSectionNumber = trim($row['A'] ?? ''); // Store section number
|
||||
$currentSection = $this->extractApplicantName($row);
|
||||
$sectionData = [];
|
||||
|
||||
// Also process the header row itself for F, G, H data
|
||||
$headerRow = $this->processDataRow($row, $bcrPercentages);
|
||||
if ($headerRow) {
|
||||
$sectionData[] = $headerRow;
|
||||
}
|
||||
|
||||
if ($isDebug) {
|
||||
$this->info("Starting new section: $currentSection");
|
||||
$this->line(" Header F: " . ($row['F'] ?? 'null'));
|
||||
$this->line(" Header G: " . ($row['G'] ?? 'null'));
|
||||
$this->line(" Header H: " . ($row['H'] ?? 'null'));
|
||||
}
|
||||
} elseif ($currentSection && $this->isDataRow($row)) {
|
||||
if ($isDebug) {
|
||||
$this->line("Found data row for section: $currentSection");
|
||||
$this->line(" Column D: " . ($row['D'] ?? 'null'));
|
||||
$this->line(" Column E: " . ($row['E'] ?? 'null'));
|
||||
$this->line(" Column F: " . ($row['F'] ?? 'null'));
|
||||
$this->line(" Column G: " . ($row['G'] ?? 'null'));
|
||||
$this->line(" Column H: " . ($row['H'] ?? 'null'));
|
||||
}
|
||||
|
||||
// Add data to current section
|
||||
$processedRow = $this->processDataRow($row, $bcrPercentages);
|
||||
if ($processedRow) {
|
||||
$sectionData[] = $processedRow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add last section
|
||||
if ($currentSection && !empty($sectionData)) {
|
||||
$sections[] = [
|
||||
'applicant_name' => $currentSection,
|
||||
'section_number' => $currentSectionNumber,
|
||||
'data' => $sectionData
|
||||
];
|
||||
}
|
||||
|
||||
return $sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if row indicates a new section/applicant
|
||||
*/
|
||||
private function isNewSection(array $row): bool
|
||||
{
|
||||
// Look for patterns that indicate a new applicant
|
||||
$firstCell = trim($row['A'] ?? '');
|
||||
|
||||
// Check for pattern like "55 / 1565", "56 / 1543", etc.
|
||||
return !empty($firstCell) && preg_match('/^\d+\s*\/\s*\d+$/', $firstCell);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract applicant name from section header
|
||||
*/
|
||||
private function extractApplicantName(array $row): string
|
||||
{
|
||||
// Row A contains number like "55 / 1565", Row B contains name and phone
|
||||
$numberPart = trim($row['A'] ?? '');
|
||||
$namePart = trim($row['B'] ?? '');
|
||||
|
||||
// Extract name from column B (remove phone number part)
|
||||
if (!empty($namePart)) {
|
||||
// Remove phone number pattern "No Telpon : xxxxx"
|
||||
$name = preg_replace('/\s*No Telpon\s*:\s*[\d\s\-\+\(\)]+.*$/i', '', $namePart);
|
||||
$name = trim($name);
|
||||
|
||||
return !empty($name) ? $name : $numberPart;
|
||||
}
|
||||
|
||||
return $numberPart ?: 'Unknown Applicant';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if row contains data
|
||||
*/
|
||||
private function isDataRow(array $row): bool
|
||||
{
|
||||
// Check if row has data we're interested in
|
||||
$columnD = trim($row['D'] ?? '');
|
||||
$columnE = trim($row['E'] ?? '');
|
||||
$columnF = trim($row['F'] ?? '');
|
||||
$columnG = trim($row['G'] ?? '');
|
||||
$columnH = trim($row['H'] ?? '');
|
||||
|
||||
// Look for important data patterns in column D
|
||||
$importantPatterns = [
|
||||
'A. Total luas lahan',
|
||||
'Total luas lahan',
|
||||
'Total Luas Lahan',
|
||||
'BCR Kawasan',
|
||||
'E. BCR Kawasan',
|
||||
'D. BCR Kawasan',
|
||||
'KWT',
|
||||
'Total KWT',
|
||||
'KWT Perumahan',
|
||||
'D. KWT Perumahan',
|
||||
'E. KWT Perumahan',
|
||||
'BCR',
|
||||
'Koefisien Wilayah Terbangun'
|
||||
];
|
||||
|
||||
foreach ($importantPatterns as $pattern) {
|
||||
if (stripos($columnD, $pattern) !== false && !empty($columnE)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for location data
|
||||
if (stripos($columnD, 'Desa') !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if any of the important columns (F, G, H) have data
|
||||
// We want to capture ALL non-empty data in these columns within a section
|
||||
if (!empty($columnF) && trim($columnF) !== '') {
|
||||
return true;
|
||||
}
|
||||
if (!empty($columnG) && trim($columnG) !== '') {
|
||||
return true;
|
||||
}
|
||||
if (!empty($columnH) && trim($columnH) !== '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a data row and calculate area using BCR formula
|
||||
*/
|
||||
private function processDataRow(array $row, array $bcrPercentages): ?array
|
||||
{
|
||||
try {
|
||||
$columnD = trim($row['D'] ?? '');
|
||||
$columnE = trim($row['E'] ?? '');
|
||||
$columnF = trim($row['F'] ?? '');
|
||||
$columnG = trim($row['G'] ?? '');
|
||||
$columnH = trim($row['H'] ?? '');
|
||||
|
||||
$landArea = 0;
|
||||
$bcrPercentage = $bcrPercentages['D'] ?? 60; // Default BCR percentage
|
||||
$location = '';
|
||||
|
||||
// Extract land area if this is a "Total luas lahan" row
|
||||
if (stripos($columnD, 'Total luas lahan') !== false ||
|
||||
stripos($columnD, 'A. Total luas lahan') !== false) {
|
||||
$landArea = $this->cleanNumericValue($columnE);
|
||||
}
|
||||
|
||||
// Extract BCR percentage if this is a BCR row - comprehensive detection
|
||||
if (stripos($columnD, 'BCR Kawasan') !== false ||
|
||||
stripos($columnD, 'E. BCR Kawasan') !== false ||
|
||||
stripos($columnD, 'D. BCR Kawasan') !== false ||
|
||||
stripos($columnD, 'KWT Perumahan') !== false ||
|
||||
stripos($columnD, 'D. KWT Perumahan') !== false ||
|
||||
stripos($columnD, 'E. KWT Perumahan') !== false ||
|
||||
stripos($columnD, 'KWT') !== false ||
|
||||
(stripos($columnD, 'BCR') !== false && stripos($columnE, '%') !== false) ||
|
||||
stripos($columnD, 'Koefisien Wilayah Terbangun') !== false) {
|
||||
$bcrValue = $this->cleanNumericValue($columnE);
|
||||
if ($bcrValue > 0 && $bcrValue <= 100) {
|
||||
$bcrPercentage = $bcrValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Get location from village/subdistrict info (previous rows in the section)
|
||||
if (stripos($columnD, 'Desa') !== false) {
|
||||
$location = $columnD;
|
||||
}
|
||||
|
||||
// Calculate area: total luas lahan dikali persentase BCR
|
||||
$calculatedArea = $landArea > 0 && $bcrPercentage > 0 ?
|
||||
round($landArea * ($bcrPercentage / 100), 2) : 0;
|
||||
|
||||
return [
|
||||
'data_type' => $columnD,
|
||||
'value' => $columnE,
|
||||
'land_area' => $landArea,
|
||||
'bcr_percentage' => $bcrPercentage,
|
||||
'calculated_area' => $calculatedArea,
|
||||
'location' => $location,
|
||||
'no_tapak' => !empty($columnF) ? $columnF : null,
|
||||
'no_skkl' => !empty($columnG) ? $columnG : null,
|
||||
'no_ukl' => !empty($columnH) ? $columnH : null,
|
||||
'raw_data' => $row
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
Log::warning("Error processing row", ['row' => $row, 'error' => $e->getMessage()]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert spatial planning data
|
||||
*/
|
||||
private function insertSpatialPlanningData(array $section): int
|
||||
{
|
||||
try {
|
||||
// Process section data to extract key values
|
||||
$sectionData = $this->consolidateSectionData($section);
|
||||
|
||||
if (empty($sectionData) || !$sectionData['has_valid_data']) {
|
||||
$this->warn("No valid data found for section: " . $section['applicant_name']);
|
||||
return 0;
|
||||
}
|
||||
|
||||
SpatialPlanning::create([
|
||||
'name' => $section['applicant_name'],
|
||||
'number' => $section['section_number'], // Kolom A - section number
|
||||
'location' => $sectionData['location'], // Column C from header row
|
||||
'land_area' => $sectionData['land_area'],
|
||||
'area' => $sectionData['calculated_area'],
|
||||
'building_function' => $sectionData['building_function'], // Determined from activities
|
||||
'sub_building_function' => $sectionData['sub_building_function'], // UMKM or Usaha Besar
|
||||
'activities' => $sectionData['activities'], // Activities from column D of first row
|
||||
'site_bcr' => $sectionData['bcr_percentage'],
|
||||
'no_tapak' => $sectionData['no_tapak'],
|
||||
'no_skkl' => $sectionData['no_skkl'],
|
||||
'no_ukl' => $sectionData['no_ukl'],
|
||||
'date' => now()->format('Y-m-d'),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now()
|
||||
]);
|
||||
|
||||
return 1;
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error inserting spatial planning data", [
|
||||
'section' => $section['applicant_name'],
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
$this->warn("Failed to insert record for: " . $section['applicant_name']);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consolidate section data into a single record
|
||||
*/
|
||||
private function consolidateSectionData(array $section): array
|
||||
{
|
||||
$landArea = 0;
|
||||
$bcrPercentage = 60; // Default from Excel file
|
||||
$location = '';
|
||||
$activities = ''; // Activities from column D of first row
|
||||
$villages = [];
|
||||
$noTapakValues = [];
|
||||
$noSKKLValues = [];
|
||||
$noUKLValues = [];
|
||||
|
||||
// Get activities from first row (header row) column D
|
||||
if (!empty($section['data']) && !empty($section['data'][0]['data_type'])) {
|
||||
$activities = $section['data'][0]['data_type']; // Column D data
|
||||
}
|
||||
|
||||
// Get location from first row (header row) column C (alamat)
|
||||
// We need to get this from raw data since processDataRow doesn't capture column C
|
||||
if (!empty($section['data']) && !empty($section['data'][0]['raw_data']['C'])) {
|
||||
$location = trim($section['data'][0]['raw_data']['C']);
|
||||
}
|
||||
|
||||
foreach ($section['data'] as $dataRow) {
|
||||
// Extract land area
|
||||
if ($dataRow['land_area'] > 0) {
|
||||
$landArea = $dataRow['land_area'];
|
||||
}
|
||||
|
||||
// Extract BCR percentage - prioritize specific BCR from this section
|
||||
// Always use section-specific BCR if found, regardless of value
|
||||
if ($dataRow['bcr_percentage'] > 0 && $dataRow['bcr_percentage'] <= 100) {
|
||||
$bcrPercentage = $dataRow['bcr_percentage'];
|
||||
}
|
||||
|
||||
// Extract additional location info from village/subdistrict data if main location is empty
|
||||
if (empty($location) && !empty($dataRow['location'])) {
|
||||
$villages[] = trim(str_replace('Desa ', '', $dataRow['location']));
|
||||
}
|
||||
|
||||
// Collect no_tapak values
|
||||
if (!empty($dataRow['no_tapak']) && !in_array($dataRow['no_tapak'], $noTapakValues)) {
|
||||
$noTapakValues[] = $dataRow['no_tapak'];
|
||||
}
|
||||
|
||||
// Collect no_skkl values
|
||||
if (!empty($dataRow['no_skkl']) && !in_array($dataRow['no_skkl'], $noSKKLValues)) {
|
||||
$noSKKLValues[] = $dataRow['no_skkl'];
|
||||
}
|
||||
|
||||
// Collect no_ukl values
|
||||
if (!empty($dataRow['no_ukl']) && !in_array($dataRow['no_ukl'], $noUKLValues)) {
|
||||
$noUKLValues[] = $dataRow['no_ukl'];
|
||||
}
|
||||
}
|
||||
|
||||
// Use first village as fallback location if main location is empty
|
||||
if (empty($location)) {
|
||||
$location = !empty($villages) ? $villages[0] : 'Unknown Location';
|
||||
}
|
||||
|
||||
// Merge multiple values with | separator
|
||||
$noTapak = !empty($noTapakValues) ? implode('|', $noTapakValues) : null;
|
||||
$noSKKL = !empty($noSKKLValues) ? implode('|', $noSKKLValues) : null;
|
||||
$noUKL = !empty($noUKLValues) ? implode('|', $noUKLValues) : null;
|
||||
|
||||
// Calculate area using BCR formula: land_area * (bcr_percentage / 100)
|
||||
$calculatedArea = $landArea > 0 && $bcrPercentage > 0 ?
|
||||
round($landArea * ($bcrPercentage / 100), 2) : 0;
|
||||
|
||||
// Determine building_function and sub_building_function based on activities and applicant name
|
||||
$buildingFunction = 'Mixed Development'; // Default
|
||||
$subBuildingFunction = null;
|
||||
|
||||
// Get applicant name for PT validation
|
||||
$applicantName = $section['applicant_name'] ?? '';
|
||||
$isCompany = (strpos($applicantName, 'PT ') === 0 || strpos($applicantName, 'PT.') === 0);
|
||||
|
||||
// Activity-based classification (priority over PT validation for specific activities)
|
||||
if (!empty($activities)) {
|
||||
$activitiesLower = strtolower($activities);
|
||||
|
||||
// 1. FUNGSI KEAGAMAAN
|
||||
if (strpos($activitiesLower, 'masjid') !== false ||
|
||||
strpos($activitiesLower, 'gereja') !== false ||
|
||||
strpos($activitiesLower, 'pura') !== false ||
|
||||
strpos($activitiesLower, 'vihara') !== false ||
|
||||
strpos($activitiesLower, 'klenteng') !== false ||
|
||||
strpos($activitiesLower, 'tempat ibadah') !== false ||
|
||||
strpos($activitiesLower, 'keagamaan') !== false ||
|
||||
strpos($activitiesLower, 'mushola') !== false) {
|
||||
|
||||
$buildingFunction = 'Fungsi Keagamaan';
|
||||
$subBuildingFunction = 'Fungsi Keagamaan';
|
||||
}
|
||||
|
||||
// 2. FUNGSI HUNIAN (PERUMAHAN) - PRIORITY HIGHER THAN PT VALIDATION
|
||||
elseif (strpos($activitiesLower, 'perumahan') !== false ||
|
||||
strpos($activitiesLower, 'perumhan') !== false ||
|
||||
strpos($activitiesLower, 'perum') !== false ||
|
||||
strpos($activitiesLower, 'rumah') !== false ||
|
||||
strpos($activitiesLower, 'hunian') !== false ||
|
||||
strpos($activitiesLower, 'residence') !== false ||
|
||||
strpos($activitiesLower, 'residential') !== false ||
|
||||
strpos($activitiesLower, 'housing') !== false ||
|
||||
strpos($activitiesLower, 'town') !== false) {
|
||||
|
||||
$buildingFunction = 'Fungsi Hunian';
|
||||
|
||||
// Determine housing type based on area and keywords
|
||||
if (strpos($activitiesLower, 'mbr') !== false ||
|
||||
strpos($activitiesLower, 'masyarakat berpenghasilan rendah') !== false ||
|
||||
strpos($activitiesLower, 'sederhana') !== false ||
|
||||
($landArea > 0 && $landArea < 2000)) { // Small area indicates MBR
|
||||
|
||||
$subBuildingFunction = 'Rumah Tinggal Deret (MBR) dan Rumah Tinggal Tunggal (MBR)';
|
||||
}
|
||||
elseif ($landArea > 0 && $landArea < 100) {
|
||||
$subBuildingFunction = 'Sederhana <100';
|
||||
}
|
||||
elseif ($landArea > 0 && $landArea > 100) {
|
||||
$subBuildingFunction = 'Tidak Sederhana >100';
|
||||
}
|
||||
else {
|
||||
$subBuildingFunction = 'Tidak Sederhana >100'; // Default for housing
|
||||
}
|
||||
}
|
||||
|
||||
// 3. FUNGSI SOSIAL BUDAYA
|
||||
elseif (strpos($activitiesLower, 'sekolah') !== false ||
|
||||
strpos($activitiesLower, 'rumah sakit') !== false ||
|
||||
strpos($activitiesLower, 'puskesmas') !== false ||
|
||||
strpos($activitiesLower, 'klinik') !== false ||
|
||||
strpos($activitiesLower, 'universitas') !== false ||
|
||||
strpos($activitiesLower, 'kampus') !== false ||
|
||||
strpos($activitiesLower, 'pendidikan') !== false ||
|
||||
strpos($activitiesLower, 'kesehatan') !== false ||
|
||||
strpos($activitiesLower, 'sosial') !== false ||
|
||||
strpos($activitiesLower, 'budaya') !== false ||
|
||||
strpos($activitiesLower, 'museum') !== false ||
|
||||
strpos($activitiesLower, 'tower') !== false ||
|
||||
strpos($activitiesLower, 'perpustakaan') !== false) {
|
||||
|
||||
$buildingFunction = 'Fungsi Sosial Budaya';
|
||||
$subBuildingFunction = 'Fungsi Sosial Budaya';
|
||||
}
|
||||
|
||||
// 4. FUNGSI USAHA
|
||||
elseif (strpos($activitiesLower, 'perdagangan') !== false ||
|
||||
strpos($activitiesLower, 'dagang') !== false ||
|
||||
strpos($activitiesLower, 'toko') !== false ||
|
||||
strpos($activitiesLower, 'usaha') !== false ||
|
||||
strpos($activitiesLower, 'komersial') !== false ||
|
||||
strpos($activitiesLower, 'pabrik') !== false ||
|
||||
strpos($activitiesLower, 'industri') !== false ||
|
||||
strpos($activitiesLower, 'manufaktur') !== false ||
|
||||
strpos($activitiesLower, 'bisnis') !== false ||
|
||||
strpos($activitiesLower, 'resto') !== false ||
|
||||
strpos($activitiesLower, 'villa') !== false ||
|
||||
strpos($activitiesLower, 'vila') !== false ||
|
||||
strpos($activitiesLower, 'gudang') !== false ||
|
||||
strpos($activitiesLower, 'pergudangan') !== false ||
|
||||
strpos($activitiesLower, 'kolam renang') !== false ||
|
||||
strpos($activitiesLower, 'minimarket') !== false ||
|
||||
strpos($activitiesLower, 'supermarket') !== false ||
|
||||
strpos($activitiesLower, 'perdaganagan') !== false ||
|
||||
strpos($activitiesLower, 'waterpark') !== false ||
|
||||
strpos($activitiesLower, 'pasar') !== false ||
|
||||
strpos($activitiesLower, 'kantor') !== false) {
|
||||
|
||||
$buildingFunction = 'Fungsi Usaha';
|
||||
|
||||
// Determine business size based on land area for non-PT businesses
|
||||
if ($landArea > 0 && $landArea > 500) { // > 500 m² considered large business
|
||||
$subBuildingFunction = 'Usaha Besar (Non-Mikro)';
|
||||
} else {
|
||||
$subBuildingFunction = 'UMKM'; // For small individual businesses
|
||||
}
|
||||
}
|
||||
|
||||
// 5. FUNGSI CAMPURAN
|
||||
elseif (strpos($activitiesLower, 'campuran') !== false ||
|
||||
strpos($activitiesLower, 'mixed') !== false ||
|
||||
strpos($activitiesLower, 'mix') !== false ||
|
||||
strpos($activitiesLower, 'multi') !== false) {
|
||||
|
||||
$buildingFunction = 'Fungsi Campuran (lebih dari 1)';
|
||||
|
||||
// Determine mixed use size
|
||||
if ($landArea > 0 && $landArea > 3000) { // > 3000 m² considered large mixed use
|
||||
$subBuildingFunction = 'Campuran Besar';
|
||||
} else {
|
||||
$subBuildingFunction = 'Campuran Kecil';
|
||||
}
|
||||
}
|
||||
// If no specific activity detected, fall back to PT validation
|
||||
else {
|
||||
// PT Company validation - PT/PT. automatically classified as Fungsi Usaha
|
||||
if ($isCompany) {
|
||||
$buildingFunction = 'Fungsi Usaha';
|
||||
|
||||
// For PT companies: area-based classification
|
||||
if ($landArea > 0 && $landArea < 500) { // < 500 m² for PT = Non-Mikro (since PT is already established business)
|
||||
$subBuildingFunction = 'Usaha Besar (Non-Mikro)';
|
||||
} elseif ($landArea >= 500) { // >= 500 m² for PT = Large Business
|
||||
$subBuildingFunction = 'Usaha Besar (Non-Mikro)';
|
||||
} else {
|
||||
$subBuildingFunction = 'Usaha Besar (Non-Mikro)'; // Default for PT
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// If no activities, fall back to PT validation
|
||||
else {
|
||||
// PT Company validation - PT/PT. automatically classified as Fungsi Usaha
|
||||
if ($isCompany) {
|
||||
$buildingFunction = 'Fungsi Usaha';
|
||||
|
||||
// For PT companies: area-based classification
|
||||
if ($landArea > 0 && $landArea < 500) { // < 500 m² for PT = Non-Mikro (since PT is already established business)
|
||||
$subBuildingFunction = 'Usaha Besar (Non-Mikro)';
|
||||
} elseif ($landArea >= 500) { // >= 500 m² for PT = Large Business
|
||||
$subBuildingFunction = 'Usaha Besar (Non-Mikro)';
|
||||
} else {
|
||||
$subBuildingFunction = 'Usaha Besar (Non-Mikro)'; // Default for PT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'land_area' => $landArea,
|
||||
'bcr_percentage' => $bcrPercentage,
|
||||
'calculated_area' => $calculatedArea,
|
||||
'location' => $location,
|
||||
'activities' => $activities, // Activities from column D of first row
|
||||
'building_function' => $buildingFunction,
|
||||
'sub_building_function' => $subBuildingFunction,
|
||||
'no_tapak' => $noTapak,
|
||||
'no_skkl' => $noSKKL,
|
||||
'no_ukl' => $noUKL,
|
||||
'has_valid_data' => $landArea > 0 // Flag untuk validasi
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean and convert string to numeric value
|
||||
*/
|
||||
private function cleanNumericValue($value): float
|
||||
{
|
||||
if (is_numeric($value)) {
|
||||
return (float) $value;
|
||||
}
|
||||
|
||||
// Remove non-numeric characters except decimal points and commas
|
||||
$cleaned = preg_replace('/[^0-9.,]/', '', $value);
|
||||
|
||||
// Handle different decimal separators
|
||||
if (strpos($cleaned, ',') !== false && strpos($cleaned, '.') !== false) {
|
||||
// Both comma and dot present, assume comma is thousands separator
|
||||
$cleaned = str_replace(',', '', $cleaned);
|
||||
} elseif (strpos($cleaned, ',') !== false) {
|
||||
// Only comma present, assume it's decimal separator
|
||||
$cleaned = str_replace(',', '.', $cleaned);
|
||||
}
|
||||
|
||||
return is_numeric($cleaned) ? (float) $cleaned : 0;
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\ScrapingDataJob;
|
||||
use App\Models\ImportDatasource;
|
||||
use App\Services\ServiceGoogleSheet;
|
||||
use App\Services\ServicePbgTask;
|
||||
use App\Services\ServiceTabPbgTask;
|
||||
use App\Services\ServiceTokenSIMBG;
|
||||
use GuzzleHttp\Client; // Import Guzzle Client
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ScrapingData extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:scraping-data';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
/**
|
||||
* Inject dependencies.
|
||||
*/
|
||||
public function __construct(
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
dispatch(new ScrapingDataJob());
|
||||
|
||||
$this->info("Scraping job dispatched successfully");
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
// public function handle()
|
||||
// {
|
||||
|
||||
// try {
|
||||
// // Create a record with "processing" status
|
||||
// $import_datasource = ImportDatasource::create([
|
||||
// 'message' => 'Initiating scraping...',
|
||||
// 'response_body' => null,
|
||||
// 'status' => 'processing',
|
||||
// 'start_time' => now()
|
||||
// ]);
|
||||
|
||||
// // Run the service
|
||||
// $service_google_sheet = new ServiceGoogleSheet();
|
||||
// $service_google_sheet->run_service();
|
||||
|
||||
// // Run the ServicePbgTask with injected Guzzle Client
|
||||
// $this->service_pbg_task->run_service();
|
||||
|
||||
// // run the service pbg task assignments
|
||||
// $this->service_tab_pbg_task->run_service();
|
||||
|
||||
// // Update the record status to "success" after completion
|
||||
// $import_datasource->update([
|
||||
// 'status' => 'success',
|
||||
// 'message' => 'Scraping completed successfully.',
|
||||
// 'finish_time' => now()
|
||||
// ]);
|
||||
|
||||
// } catch (\Exception $e) {
|
||||
|
||||
// // Log the error for debugging
|
||||
// Log::error('Scraping failed: ' . $e->getMessage(), ['trace' => $e->getTraceAsString()]);
|
||||
|
||||
// // Handle errors by updating the status to "failed"
|
||||
// if (isset($import_datasource)) {
|
||||
// $import_datasource->update([
|
||||
// 'status' => 'failed',
|
||||
// 'response_body' => 'Error: ' . $e->getMessage(),
|
||||
// 'finish_time' => now()
|
||||
// ]);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
33
app/Console/Commands/ScrapingLeaderData.php
Normal file
33
app/Console/Commands/ScrapingLeaderData.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\ServiceGoogleSheet;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ScrapingLeaderData extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:scraping-leader-data';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Scraping leader data from google spreadsheet and save to database';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$service_google_sheet = app(ServiceGoogleSheet::class);
|
||||
$service_google_sheet->sync_leader_data();
|
||||
$this->info('Leader data synced successfully');
|
||||
}
|
||||
}
|
||||
80
app/Console/Commands/StartScrapingData.php
Normal file
80
app/Console/Commands/StartScrapingData.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\ScrapingDataJob;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class StartScrapingData extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:start-scraping-data
|
||||
{--confirm : Skip confirmation prompt}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Start the optimized scraping data job (Google Sheet -> PBG Task -> Details)';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('🚀 Starting Optimized Scraping Data Job');
|
||||
$this->info('=====================================');
|
||||
|
||||
if (!$this->option('confirm')) {
|
||||
$this->warn('⚠️ This will start a comprehensive data scraping process:');
|
||||
$this->line(' 1. Google Sheet data scraping');
|
||||
$this->line(' 2. PBG Task parent data scraping');
|
||||
$this->line(' 3. Detailed task information scraping');
|
||||
$this->line(' 4. BigData resume generation');
|
||||
$this->newLine();
|
||||
|
||||
if (!$this->confirm('Do you want to continue?')) {
|
||||
$this->info('Operation cancelled.');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Dispatch the optimized job
|
||||
$job = new ScrapingDataJob();
|
||||
dispatch($job);
|
||||
|
||||
Log::info('ScrapingDataJob dispatched via command', [
|
||||
'command' => $this->signature,
|
||||
'user' => $this->option('confirm') ? 'auto' : 'manual'
|
||||
]);
|
||||
|
||||
$this->info('✅ Scraping Data Job has been dispatched to the scraping queue!');
|
||||
$this->newLine();
|
||||
$this->info('📊 Monitor the job with:');
|
||||
$this->line(' php artisan queue:monitor scraping');
|
||||
$this->newLine();
|
||||
$this->info('📜 View detailed logs with:');
|
||||
$this->line(' tail -f /var/log/supervisor/sibedas-queue-scraping.log | grep "SCRAPING DATA JOB"');
|
||||
$this->newLine();
|
||||
$this->info('🔍 Check ImportDatasource status:');
|
||||
$this->line(' docker-compose -f docker-compose.local.yml exec app php artisan tinker --execute="App\\Models\\ImportDatasource::latest()->first();"');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error('❌ Failed to dispatch ScrapingDataJob: ' . $e->getMessage());
|
||||
Log::error('Failed to dispatch ScrapingDataJob via command', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
62
app/Console/Commands/SyncDashboardPbg.php
Normal file
62
app/Console/Commands/SyncDashboardPbg.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Services\ServiceGoogleSheet;
|
||||
use App\Models\BigdataResume;
|
||||
use App\Models\ImportDatasource;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SyncDashboardPbg extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:sync-dashboard-pbg';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$import_datasource = ImportDatasource::create([
|
||||
'message' => 'Initiating sync dashboard pbg...',
|
||||
'response_body' => null,
|
||||
'status' => 'processing',
|
||||
'start_time' => now(),
|
||||
'failed_uuid' => null
|
||||
]);
|
||||
|
||||
try {
|
||||
BigdataResume::generateResumeData($import_datasource->id, date('Y'), "simbg");
|
||||
|
||||
$import_datasource->update([
|
||||
'status' => 'success',
|
||||
'message' => 'Sync dashboard pbg completed successfully.',
|
||||
'finish_time' => now()
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Sync dashboard pbg failed: ' . $e->getMessage(), ['trace' => $e->getTraceAsString()]);
|
||||
|
||||
// Update status to failed
|
||||
if (isset($import_datasource)) {
|
||||
$import_datasource->update([
|
||||
'status' => 'failed',
|
||||
'message' => 'Sync dashboard pbg failed.',
|
||||
'finish_time' => now()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
98
app/Console/Commands/SyncGoogleSheetData.php
Normal file
98
app/Console/Commands/SyncGoogleSheetData.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\ServiceGoogleSheet;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Exception;
|
||||
|
||||
class SyncGoogleSheetData extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'sync:google-sheet {--type=all : Specify sync type (all, google-sheet, big-data, leader)}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Sync data from Google Sheets to database';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$type = $this->option('type');
|
||||
|
||||
$this->info('Starting Google Sheet data synchronization...');
|
||||
$this->info("Sync type: {$type}");
|
||||
|
||||
try {
|
||||
$service = new ServiceGoogleSheet();
|
||||
|
||||
switch ($type) {
|
||||
case 'google-sheet':
|
||||
$this->info('Syncing Google Sheet data...');
|
||||
$service->sync_google_sheet_data();
|
||||
$this->info('✅ Google Sheet data synchronized successfully!');
|
||||
break;
|
||||
|
||||
case 'big-data':
|
||||
$this->info('Syncing Big Data...');
|
||||
$service->sync_big_data();
|
||||
$this->info('✅ Big Data synchronized successfully!');
|
||||
break;
|
||||
|
||||
case 'leader':
|
||||
$this->info('Syncing Leader data...');
|
||||
$result = $service->sync_leader_data();
|
||||
$this->info('✅ Leader data synchronized successfully!');
|
||||
$this->table(['Section', 'Total', 'Nominal'], collect($result)->map(function($item, $key) {
|
||||
// Convert nominal to numeric before formatting
|
||||
$nominal = $item['nominal'] ?? 0;
|
||||
if (is_string($nominal)) {
|
||||
// Remove dots and convert to float
|
||||
$nominal = (float) str_replace('.', '', $nominal);
|
||||
}
|
||||
|
||||
return [
|
||||
$key,
|
||||
$item['total'] ?? 'N/A',
|
||||
number_format((float) $nominal, 0, ',', '.')
|
||||
];
|
||||
})->toArray());
|
||||
break;
|
||||
|
||||
case 'all':
|
||||
default:
|
||||
$this->info('Syncing all data (Google Sheet + Big Data)...');
|
||||
$service->run_service();
|
||||
$this->info('✅ All data synchronized successfully!');
|
||||
break;
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info('🚀 Synchronization completed successfully!');
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->error('❌ Synchronization failed!');
|
||||
$this->error("Error: {$e->getMessage()}");
|
||||
|
||||
Log::error('Google Sheet sync command failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'type' => $type,
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
65
app/Console/Commands/SyncPbgTaskPayments.php
Normal file
65
app/Console/Commands/SyncPbgTaskPayments.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\ServiceGoogleSheet;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class SyncPbgTaskPayments extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'sync:pbg-payments';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Sync PBG task payments from Google Sheets Sheet Data';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('🚀 Starting PBG Task Payments sync...');
|
||||
$this->newLine();
|
||||
|
||||
try {
|
||||
$service = new ServiceGoogleSheet();
|
||||
|
||||
// Show progress bar
|
||||
$this->info('📊 Fetching data from Google Sheets...');
|
||||
$result = $service->sync_pbg_task_payments();
|
||||
|
||||
// Display results
|
||||
$this->newLine();
|
||||
$this->info('✅ Sync completed successfully!');
|
||||
$this->newLine();
|
||||
|
||||
$this->table(
|
||||
['Metric', 'Value'],
|
||||
[
|
||||
['Inserted rows', $result['inserted'] ?? 0],
|
||||
['Success', ($result['success'] ?? false) ? 'Yes' : 'No'],
|
||||
]
|
||||
);
|
||||
|
||||
$this->newLine();
|
||||
$this->info('📝 Check Laravel logs for detailed information.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->newLine();
|
||||
$this->error('❌ Sync failed!');
|
||||
$this->error('Error: ' . $e->getMessage());
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
265
app/Console/Commands/TestRetributionCalculation.php
Normal file
265
app/Console/Commands/TestRetributionCalculation.php
Normal file
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Services\RetributionCalculatorService;
|
||||
use App\Models\BuildingType;
|
||||
|
||||
class TestRetributionCalculation extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'retribution:test
|
||||
{--area= : Luas bangunan dalam m2}
|
||||
{--floor= : Jumlah lantai (1-6)}
|
||||
{--type= : ID atau kode building type}
|
||||
{--all : Test semua building types}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Test perhitungan retribusi PBG dengan input luas bangunan dan tinggi lantai';
|
||||
|
||||
protected $calculatorService;
|
||||
|
||||
public function __construct(RetributionCalculatorService $calculatorService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->calculatorService = $calculatorService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('🏢 SISTEM TEST PERHITUNGAN RETRIBUSI PBG');
|
||||
$this->info('=' . str_repeat('=', 50));
|
||||
|
||||
// Test all building types if --all flag is used
|
||||
if ($this->option('all')) {
|
||||
return $this->testAllBuildingTypes();
|
||||
}
|
||||
|
||||
// Get input parameters
|
||||
$area = $this->getArea();
|
||||
$floor = $this->getFloor();
|
||||
$buildingTypeId = $this->getBuildingType();
|
||||
|
||||
if (!$area || !$floor || !$buildingTypeId) {
|
||||
$this->error('❌ Parameter tidak lengkap!');
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Perform calculation
|
||||
$this->performCalculation($buildingTypeId, $floor, $area);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected function getArea()
|
||||
{
|
||||
$area = $this->option('area');
|
||||
|
||||
if (!$area) {
|
||||
$area = $this->ask('📐 Masukkan luas bangunan (m²)');
|
||||
}
|
||||
|
||||
if (!is_numeric($area) || $area <= 0) {
|
||||
$this->error('❌ Luas bangunan harus berupa angka positif!');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (float) $area;
|
||||
}
|
||||
|
||||
protected function getFloor()
|
||||
{
|
||||
$floor = $this->option('floor');
|
||||
|
||||
if (!$floor) {
|
||||
$floor = $this->ask('🏗️ Masukkan jumlah lantai (1-6)');
|
||||
}
|
||||
|
||||
if (!is_numeric($floor) || $floor < 1 || $floor > 6) {
|
||||
$this->error('❌ Jumlah lantai harus antara 1-6!');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $floor;
|
||||
}
|
||||
|
||||
protected function getBuildingType()
|
||||
{
|
||||
$type = $this->option('type');
|
||||
|
||||
if (!$type) {
|
||||
$this->showBuildingTypes();
|
||||
$type = $this->ask('🏢 Masukkan ID atau kode building type');
|
||||
}
|
||||
|
||||
// Try to find by ID first, then by code
|
||||
$buildingType = null;
|
||||
|
||||
if (is_numeric($type)) {
|
||||
$buildingType = BuildingType::find($type);
|
||||
} else {
|
||||
$buildingType = BuildingType::where('code', strtoupper($type))->first();
|
||||
}
|
||||
|
||||
if (!$buildingType) {
|
||||
$this->error('❌ Building type tidak ditemukan!');
|
||||
return null;
|
||||
}
|
||||
|
||||
return $buildingType->id;
|
||||
}
|
||||
|
||||
protected function showBuildingTypes()
|
||||
{
|
||||
$this->info('📋 DAFTAR BUILDING TYPES:');
|
||||
$this->line('');
|
||||
|
||||
$buildingTypes = BuildingType::with('indices')
|
||||
->whereHas('indices') // Only types that have indices
|
||||
->get();
|
||||
|
||||
$headers = ['ID', 'Kode', 'Nama', 'Coefficient', 'Free'];
|
||||
$rows = [];
|
||||
|
||||
foreach ($buildingTypes as $type) {
|
||||
$rows[] = [
|
||||
$type->id,
|
||||
$type->code,
|
||||
$type->name,
|
||||
$type->indices ? number_format($type->indices->coefficient, 4) : 'N/A',
|
||||
$type->is_free ? '✅' : '❌'
|
||||
];
|
||||
}
|
||||
|
||||
$this->table($headers, $rows);
|
||||
$this->line('');
|
||||
}
|
||||
|
||||
protected function performCalculation($buildingTypeId, $floor, $area)
|
||||
{
|
||||
try {
|
||||
// Round area to 2 decimal places to match database storage format
|
||||
$roundedArea = round($area, 2);
|
||||
$result = $this->calculatorService->calculate($buildingTypeId, $floor, $roundedArea, false);
|
||||
|
||||
$this->displayResults($result, $roundedArea, $floor);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error('❌ Error: ' . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
protected function displayResults($result, $area, $floor)
|
||||
{
|
||||
$this->info('');
|
||||
$this->info('📊 HASIL PERHITUNGAN RETRIBUSI');
|
||||
$this->info('=' . str_repeat('=', 40));
|
||||
|
||||
// Building info
|
||||
$this->line('🏢 <fg=cyan>Building Type:</> ' . $result['building_type']['name']);
|
||||
$this->line('📐 <fg=cyan>Luas Bangunan:</> ' . number_format($area, 0) . ' m²');
|
||||
$this->line('🏗️ <fg=cyan>Jumlah Lantai:</> ' . $floor);
|
||||
|
||||
if (isset($result['building_type']['is_free']) && $result['building_type']['is_free']) {
|
||||
$this->line('');
|
||||
$this->info('🎉 GRATIS - Building type ini tidak dikenakan retribusi');
|
||||
$this->line('💰 <fg=green>Total Retribusi: Rp 0</fg=green>');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->line('');
|
||||
|
||||
// Parameters
|
||||
$this->info('📋 PARAMETER PERHITUNGAN:');
|
||||
$indices = $result['indices'];
|
||||
$this->line('• Coefficient: ' . number_format($indices['coefficient'], 4));
|
||||
$this->line('• IP Permanent: ' . number_format($indices['ip_permanent'], 4));
|
||||
$this->line('• IP Complexity: ' . number_format($indices['ip_complexity'], 4));
|
||||
$this->line('• Locality Index: ' . number_format($indices['locality_index'], 4));
|
||||
$this->line('• Height Index: ' . number_format($result['input_parameters']['height_index'], 4));
|
||||
|
||||
$this->line('');
|
||||
|
||||
// Calculation steps
|
||||
$this->info('🔢 LANGKAH PERHITUNGAN:');
|
||||
$detail = $result['calculation_detail'];
|
||||
$this->line('1. H5 Raw: ' . number_format($detail['h5_raw'], 6));
|
||||
$this->line('2. H5 Rounded: ' . number_format($detail['h5'], 4));
|
||||
$this->line('3. Main Calculation: Rp ' . number_format($detail['main'], 2));
|
||||
$this->line('4. Infrastructure (50%): Rp ' . number_format($detail['infrastructure'], 2));
|
||||
|
||||
$this->line('');
|
||||
|
||||
// Final result
|
||||
$this->info('💰 <fg=green>TOTAL RETRIBUSI: ' . $result['formatted_amount'] . '</fg=green>');
|
||||
$this->line('📈 <fg=yellow>Per m²: Rp ' . number_format($result['total_retribution'] / $area, 2) . '</fg=yellow>');
|
||||
}
|
||||
|
||||
protected function testAllBuildingTypes()
|
||||
{
|
||||
$area = round($this->option('area') ?: 100, 2);
|
||||
$floor = $this->option('floor') ?: 2;
|
||||
|
||||
$this->info("🧪 TESTING SEMUA BUILDING TYPES");
|
||||
$this->info("📐 Luas: {$area} m² | 🏗️ Lantai: {$floor}");
|
||||
$this->info('=' . str_repeat('=', 60));
|
||||
|
||||
$buildingTypes = BuildingType::with('indices')
|
||||
->whereHas('indices') // Only types that have indices
|
||||
->orderBy('level')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
$headers = ['Kode', 'Nama', 'Coefficient', 'Total Retribusi', 'Per m²'];
|
||||
$rows = [];
|
||||
|
||||
foreach ($buildingTypes as $type) {
|
||||
try {
|
||||
$result = $this->calculatorService->calculate($type->id, $floor, $area, false);
|
||||
|
||||
if ($type->is_free) {
|
||||
$rows[] = [
|
||||
$type->code,
|
||||
$type->name,
|
||||
'FREE',
|
||||
'Rp 0',
|
||||
'Rp 0'
|
||||
];
|
||||
} else {
|
||||
$rows[] = [
|
||||
$type->code,
|
||||
$type->name,
|
||||
number_format($result['indices']['coefficient'], 4),
|
||||
'Rp ' . number_format($result['total_retribution'], 0),
|
||||
'Rp ' . number_format($result['total_retribution'] / $area, 0)
|
||||
];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$rows[] = [
|
||||
$type->code,
|
||||
$type->name,
|
||||
'ERROR',
|
||||
$e->getMessage(),
|
||||
'-'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$this->table($headers, $rows);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
42
app/Console/Commands/TruncatePBGTable.php
Normal file
42
app/Console/Commands/TruncatePBGTable.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\PbgTask;
|
||||
use App\Models\PbgTaskDetail;
|
||||
use App\Models\PbgTaskIndexIntegrations;
|
||||
use App\Models\PbgTaskPrasarana;
|
||||
use App\Models\PbgTaskRetributions;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TruncatePBGTable extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:truncate-pbg-table';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
|
||||
PbgTask::truncate();
|
||||
PbgTaskRetributions::truncate();
|
||||
PbgTaskDetail::truncate();
|
||||
PbgTaskIndexIntegrations::truncate();
|
||||
PbgTaskPrasarana::truncate();
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
|
||||
}
|
||||
}
|
||||
266
app/Console/Commands/TruncateSpatialPlanningData.php
Normal file
266
app/Console/Commands/TruncateSpatialPlanningData.php
Normal file
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Exception;
|
||||
|
||||
class TruncateSpatialPlanningData extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'spatial-planning:truncate
|
||||
{--force : Force truncate without confirmation}
|
||||
{--backup : Create backup before truncate}
|
||||
{--dry-run : Show what would be truncated without actually doing it}
|
||||
{--only-active : Only truncate active calculations}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Truncate spatial planning data with related calculable retributions and retribution calculations';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
$force = $this->option('force');
|
||||
$backup = $this->option('backup');
|
||||
$dryRun = $this->option('dry-run');
|
||||
$onlyActive = $this->option('only-active');
|
||||
|
||||
if ($dryRun) {
|
||||
$this->warn('DRY RUN MODE - No data will be truncated');
|
||||
}
|
||||
|
||||
// Check existing data
|
||||
$this->showDataStatistics();
|
||||
|
||||
// Confirm truncation if not in force mode
|
||||
if (!$force && !$dryRun) {
|
||||
if (!$this->confirm('This will permanently delete all spatial planning data and related calculations. Continue?')) {
|
||||
$this->info('Operation cancelled.');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Create backup if requested
|
||||
if ($backup && !$dryRun) {
|
||||
$this->createBackup();
|
||||
}
|
||||
|
||||
// Perform truncation
|
||||
if ($dryRun) {
|
||||
$this->performDryRun();
|
||||
} else {
|
||||
$this->performTruncation($onlyActive);
|
||||
}
|
||||
|
||||
// Show final statistics
|
||||
if (!$dryRun) {
|
||||
$this->showDataStatistics('AFTER');
|
||||
}
|
||||
|
||||
return 0;
|
||||
} catch (Exception $e) {
|
||||
$this->error("Error: " . $e->getMessage());
|
||||
Log::error("TruncateSpatialPlanningData failed", ['error' => $e->getMessage()]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show data statistics
|
||||
*/
|
||||
private function showDataStatistics(string $prefix = 'BEFORE'): void
|
||||
{
|
||||
$this->info("=== {$prefix} TRUNCATION ===");
|
||||
|
||||
$spatialCount = DB::table('spatial_plannings')->count();
|
||||
$calculableCount = DB::table('calculable_retributions')->count();
|
||||
$activeCalculableCount = DB::table('calculable_retributions')->where('is_active', true)->count();
|
||||
$calculationCount = DB::table('retribution_calculations')->count();
|
||||
|
||||
$this->table(
|
||||
['Table', 'Total Records', 'Active Records'],
|
||||
[
|
||||
['spatial_plannings', $spatialCount, '-'],
|
||||
['calculable_retributions', $calculableCount, $activeCalculableCount],
|
||||
['retribution_calculations', $calculationCount, '-'],
|
||||
]
|
||||
);
|
||||
|
||||
// Show breakdown by building function
|
||||
$buildingFunctionStats = DB::table('spatial_plannings')
|
||||
->select('building_function', DB::raw('count(*) as total'))
|
||||
->groupBy('building_function')
|
||||
->get();
|
||||
|
||||
if ($buildingFunctionStats->isNotEmpty()) {
|
||||
$this->info('Building Function Breakdown:');
|
||||
foreach ($buildingFunctionStats as $stat) {
|
||||
$this->line(" - {$stat->building_function}: {$stat->total} records");
|
||||
}
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create backup of data
|
||||
*/
|
||||
private function createBackup(): void
|
||||
{
|
||||
$this->info('Creating backup...');
|
||||
|
||||
$timestamp = date('Y-m-d_H-i-s');
|
||||
$backupPath = storage_path("backups/spatial_planning_backup_{$timestamp}");
|
||||
|
||||
// Create backup directory
|
||||
if (!file_exists($backupPath)) {
|
||||
mkdir($backupPath, 0755, true);
|
||||
}
|
||||
|
||||
// Backup spatial plannings
|
||||
$spatialData = DB::table('spatial_plannings')->get();
|
||||
file_put_contents(
|
||||
"{$backupPath}/spatial_plannings.json",
|
||||
$spatialData->toJson(JSON_PRETTY_PRINT)
|
||||
);
|
||||
|
||||
// Backup calculable retributions
|
||||
$calculableData = DB::table('calculable_retributions')->get();
|
||||
file_put_contents(
|
||||
"{$backupPath}/calculable_retributions.json",
|
||||
$calculableData->toJson(JSON_PRETTY_PRINT)
|
||||
);
|
||||
|
||||
// Backup retribution calculations
|
||||
$calculationData = DB::table('retribution_calculations')->get();
|
||||
file_put_contents(
|
||||
"{$backupPath}/retribution_calculations.json",
|
||||
$calculationData->toJson(JSON_PRETTY_PRINT)
|
||||
);
|
||||
|
||||
$this->info("Backup created at: {$backupPath}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform dry run
|
||||
*/
|
||||
private function performDryRun(): void
|
||||
{
|
||||
$this->info('DRY RUN - Would truncate the following:');
|
||||
|
||||
$spatialCount = DB::table('spatial_plannings')->count();
|
||||
$calculableCount = DB::table('calculable_retributions')->count();
|
||||
$calculationCount = DB::table('retribution_calculations')->count();
|
||||
|
||||
$this->line(" - spatial_plannings: {$spatialCount} records");
|
||||
$this->line(" - calculable_retributions: {$calculableCount} records");
|
||||
$this->line(" - retribution_calculations: {$calculationCount} records");
|
||||
|
||||
$this->warn('No actual data was truncated (dry run mode)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform actual truncation
|
||||
*/
|
||||
private function performTruncation(bool $onlyActive = false): void
|
||||
{
|
||||
$this->info('Starting truncation...');
|
||||
|
||||
try {
|
||||
// Disable foreign key checks for safe truncation
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
|
||||
|
||||
if ($onlyActive) {
|
||||
// Only truncate active calculations
|
||||
$this->truncateActiveOnly();
|
||||
} else {
|
||||
// Truncate all data
|
||||
$this->truncateAllData();
|
||||
}
|
||||
|
||||
// Re-enable foreign key checks
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
|
||||
|
||||
$this->info('✅ Truncation completed successfully!');
|
||||
|
||||
} catch (Exception $e) {
|
||||
// Make sure to re-enable foreign key checks even on error
|
||||
try {
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
|
||||
} catch (Exception $fkError) {
|
||||
$this->error('Failed to re-enable foreign key checks: ' . $fkError->getMessage());
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate only active calculations
|
||||
*/
|
||||
private function truncateActiveOnly(): void
|
||||
{
|
||||
$this->info('Truncating only active calculations...');
|
||||
|
||||
// Delete active calculable retributions
|
||||
$deletedActive = DB::table('calculable_retributions')
|
||||
->where('is_active', true)
|
||||
->delete();
|
||||
$this->info("Deleted {$deletedActive} active calculable retributions");
|
||||
|
||||
// Delete orphaned retribution calculations
|
||||
$deletedOrphaned = DB::table('retribution_calculations')
|
||||
->whereNotExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('calculable_retributions')
|
||||
->whereColumn('calculable_retributions.retribution_calculation_id', 'retribution_calculations.id');
|
||||
})
|
||||
->delete();
|
||||
$this->info("Deleted {$deletedOrphaned} orphaned retribution calculations");
|
||||
|
||||
// Keep spatial plannings but remove their calculation relationships
|
||||
$this->info('Spatial plannings data preserved (only calculations removed)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate all data
|
||||
*/
|
||||
private function truncateAllData(): void
|
||||
{
|
||||
$this->info('Truncating all data...');
|
||||
|
||||
// Get counts before truncation
|
||||
$spatialCount = DB::table('spatial_plannings')->count();
|
||||
$calculableCount = DB::table('calculable_retributions')->count();
|
||||
$calculationCount = DB::table('retribution_calculations')->count();
|
||||
|
||||
// Truncate tables in correct order
|
||||
DB::table('calculable_retributions')->truncate();
|
||||
$this->info("Truncated calculable_retributions table ({$calculableCount} records)");
|
||||
|
||||
DB::table('retribution_calculations')->truncate();
|
||||
$this->info("Truncated retribution_calculations table ({$calculationCount} records)");
|
||||
|
||||
DB::table('spatial_plannings')->truncate();
|
||||
$this->info("Truncated spatial_plannings table ({$spatialCount} records)");
|
||||
|
||||
// Reset auto increment
|
||||
DB::statement('ALTER TABLE calculable_retributions AUTO_INCREMENT = 1');
|
||||
DB::statement('ALTER TABLE retribution_calculations AUTO_INCREMENT = 1');
|
||||
DB::statement('ALTER TABLE spatial_plannings AUTO_INCREMENT = 1');
|
||||
$this->info('Reset auto increment counters');
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,32 @@ enum PbgTaskFilterData : string
|
||||
case verified = 'verified';
|
||||
case non_verified = 'non-verified';
|
||||
case all = 'all';
|
||||
case potention = 'potention';
|
||||
case issuance_realization_pbg = 'issuance-realization-pbg';
|
||||
case process_in_technical_office = 'process-in-technical-office';
|
||||
case waiting_click_dpmptsp = 'waiting-click-dpmptsp';
|
||||
case non_business_rab = 'non-business-rab';
|
||||
case non_business_krk = 'non-business-krk';
|
||||
case business_rab = 'business-rab';
|
||||
case business_krk = 'business-krk';
|
||||
case business_dlh = 'business-dlh';
|
||||
|
||||
public static function getAllOptions() : array {
|
||||
return [
|
||||
self::all->value => 'Potensi Berkas',
|
||||
self::all->value => 'Semua Berkas',
|
||||
self::business->value => 'Usaha',
|
||||
self::non_business->value => 'Bukan Usaha',
|
||||
self::verified->value => 'Terverifikasi',
|
||||
self::non_verified->value => 'Belum Terverifikasi',
|
||||
self::potention->value => 'Potensi',
|
||||
self::issuance_realization_pbg->value => 'Realisasi PBG',
|
||||
self::process_in_technical_office->value => 'Proses Di Dinas Teknis',
|
||||
self::waiting_click_dpmptsp->value => 'Menunggu Klik DPMPTSP',
|
||||
self::non_business_rab->value => 'Non Usaha - RAB',
|
||||
self::non_business_krk->value => 'Non Usaha - KRK',
|
||||
self::business_rab->value => 'Usaha - RAB',
|
||||
self::business_krk->value => 'Usaha - KRK',
|
||||
self::business_dlh->value => 'Usaha - DLH',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,4 +56,90 @@ enum PbgTaskStatus: int
|
||||
{
|
||||
return self::getStatuses()[$status] ?? null;
|
||||
}
|
||||
|
||||
public static function getWaitingClickDpmptsp(): array
|
||||
{
|
||||
return [
|
||||
self::MENUNGGU_PEMBAYARAN_RETRIBUSI->value,
|
||||
self::PROSES_PENERBITAN_SKRD->value,
|
||||
self::VERIFIKASI_PEMBAYARAN_RETRIBUSI->value
|
||||
];
|
||||
}
|
||||
|
||||
public static function getIssuanceRealizationPbg(): array
|
||||
{
|
||||
return [
|
||||
self::PENERBITAN_SK_PBG->value,
|
||||
self::SK_PBG_TERBIT->value,
|
||||
self::VERIFIKASI_SK_PBG->value
|
||||
];
|
||||
}
|
||||
|
||||
public static function getProcessInTechnicalOffice(): array
|
||||
{
|
||||
return [
|
||||
self::PENERBITAN_SPPST->value,
|
||||
self::PERHITUNGAN_RETRIBUSI->value,
|
||||
self::RETRIBUSI_TIDAK_SESUAI->value,
|
||||
self::MENUNGGU_JADWAL_KONSULTASI->value,
|
||||
self::MENUNGGU_PENUGASAN_TPT_TPA->value,
|
||||
self::PELAKSANAAN_KONSULTASI->value
|
||||
];
|
||||
}
|
||||
|
||||
public static function getVerified(): array
|
||||
{
|
||||
return [
|
||||
self::MENUNGGU_PEMBAYARAN_RETRIBUSI->value,
|
||||
self::PROSES_PENERBITAN_SKRD->value,
|
||||
self::VERIFIKASI_PEMBAYARAN_RETRIBUSI->value,
|
||||
self::PENERBITAN_SK_PBG->value,
|
||||
self::SK_PBG_TERBIT->value,
|
||||
self::VERIFIKASI_SK_PBG->value,
|
||||
self::PENERBITAN_SPPST->value,
|
||||
self::PERHITUNGAN_RETRIBUSI->value,
|
||||
self::RETRIBUSI_TIDAK_SESUAI->value,
|
||||
self::MENUNGGU_JADWAL_KONSULTASI->value,
|
||||
self::MENUNGGU_PENUGASAN_TPT_TPA->value,
|
||||
self::PELAKSANAAN_KONSULTASI->value
|
||||
];
|
||||
}
|
||||
|
||||
public static function getNonVerified(): array
|
||||
{
|
||||
return [
|
||||
self::VERIFIKASI_KELENGKAPAN->value,
|
||||
self::PERBAIKAN_DOKUMEN->value,
|
||||
self::PERBAIKAN_DOKUMEN_KONSULTASI->value,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPotention(): array
|
||||
{
|
||||
return [
|
||||
self::MENUNGGU_PEMBAYARAN_RETRIBUSI->value,
|
||||
self::PROSES_PENERBITAN_SKRD->value,
|
||||
self::VERIFIKASI_PEMBAYARAN_RETRIBUSI->value,
|
||||
self::PENERBITAN_SK_PBG->value,
|
||||
self::SK_PBG_TERBIT->value,
|
||||
self::VERIFIKASI_SK_PBG->value,
|
||||
self::PENERBITAN_SPPST->value,
|
||||
self::PERHITUNGAN_RETRIBUSI->value,
|
||||
self::RETRIBUSI_TIDAK_SESUAI->value,
|
||||
self::MENUNGGU_JADWAL_KONSULTASI->value,
|
||||
self::MENUNGGU_PENUGASAN_TPT_TPA->value,
|
||||
self::PELAKSANAAN_KONSULTASI->value,
|
||||
self::VERIFIKASI_KELENGKAPAN->value,
|
||||
self::PERBAIKAN_DOKUMEN->value,
|
||||
self::PERBAIKAN_DOKUMEN_KONSULTASI->value,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getRejected(): array
|
||||
{
|
||||
return [
|
||||
self::PERMOHONAN_DITOLAK->value,
|
||||
self::PERMOHONAN_DIBATALKAN->value
|
||||
];
|
||||
}
|
||||
}
|
||||
118
app/Exports/PbgTaskExport.php
Normal file
118
app/Exports/PbgTaskExport.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use App\Models\PbgTask;
|
||||
use App\Enums\PbgTaskFilterData;
|
||||
|
||||
class PbgTaskExport implements FromCollection, WithHeadings
|
||||
{
|
||||
protected $category;
|
||||
protected $year;
|
||||
|
||||
public function __construct(string $category, int $year)
|
||||
{
|
||||
$this->category = $category;
|
||||
$this->year = $year;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public function collection()
|
||||
{
|
||||
$query = PbgTask::query()
|
||||
->whereYear('task_created_at', $this->year);
|
||||
|
||||
// Menggunakan switch case karena lebih readable dan maintainable
|
||||
// untuk multiple conditions yang berbeda
|
||||
switch ($this->category) {
|
||||
case PbgTaskFilterData::all->value:
|
||||
// Tidak ada filter tambahan, ambil semua data
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::business->value:
|
||||
$query->where('application_type', 'business');
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::non_business->value:
|
||||
$query->where('application_type', 'non-business');
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::verified->value:
|
||||
$query->where('is_valid', true);
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::non_verified->value:
|
||||
$query->where('is_valid', false);
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::potention->value:
|
||||
$query->where('status', 'potention');
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::issuance_realization_pbg->value:
|
||||
$query->where('status', 'issuance-realization-pbg');
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::process_in_technical_office->value:
|
||||
$query->where('status', 'process-in-technical-office');
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::waiting_click_dpmptsp->value:
|
||||
$query->where('status', 'waiting-click-dpmptsp');
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::non_business_rab->value:
|
||||
$query->where('application_type', 'non-business')
|
||||
->where('consultation_type', 'rab');
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::non_business_krk->value:
|
||||
$query->where('application_type', 'non-business')
|
||||
->where('consultation_type', 'krk');
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::business_rab->value:
|
||||
$query->where('application_type', 'business')
|
||||
->where('consultation_type', 'rab');
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::business_krk->value:
|
||||
$query->where('application_type', 'business')
|
||||
->where('consultation_type', 'krk');
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::business_dlh->value:
|
||||
$query->where('application_type', 'business')
|
||||
->where('consultation_type', 'dlh');
|
||||
break;
|
||||
|
||||
default:
|
||||
// Jika category tidak dikenali, return empty collection
|
||||
return collect();
|
||||
}
|
||||
|
||||
return $query->select([
|
||||
'registration_number',
|
||||
'document_number',
|
||||
'owner_name',
|
||||
'address',
|
||||
'name as building_name',
|
||||
'function_type'
|
||||
])->get();
|
||||
}
|
||||
|
||||
public function headings(): array{
|
||||
return [
|
||||
'Nomor Registrasi',
|
||||
'Nomor Dokumen',
|
||||
'Nama Pemilik',
|
||||
'Alamat Pemilik',
|
||||
'Nama Bangunan',
|
||||
'Fungsi Bangunan',
|
||||
];
|
||||
}
|
||||
}
|
||||
59
app/Exports/TaxSubdistrictSheetExport.php
Normal file
59
app/Exports/TaxSubdistrictSheetExport.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use App\Models\Tax;
|
||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
|
||||
class TaxSubdistrictSheetExport implements FromCollection, WithTitle, WithHeadings
|
||||
{
|
||||
protected $subdistrict;
|
||||
|
||||
public function __construct(string $subdistrict)
|
||||
{
|
||||
$this->subdistrict = $subdistrict;
|
||||
}
|
||||
|
||||
public function collection()
|
||||
{
|
||||
return Tax::where('subdistrict', $this->subdistrict)
|
||||
->select(
|
||||
'tax_code',
|
||||
'tax_no',
|
||||
'npwpd',
|
||||
'wp_name',
|
||||
'business_name',
|
||||
'address',
|
||||
'start_validity',
|
||||
'end_validity',
|
||||
'tax_value',
|
||||
'subdistrict',
|
||||
'village'
|
||||
)->get();
|
||||
}
|
||||
|
||||
public function headings(): array
|
||||
{
|
||||
return [
|
||||
'Kode',
|
||||
'No',
|
||||
'NPWPD',
|
||||
'Nama WP',
|
||||
'Nama Usaha',
|
||||
'Alamat Usaha',
|
||||
'Tanggal Mulai Berlaku',
|
||||
'Tanggal Berakhir Berlaku',
|
||||
'Nilai Pajak',
|
||||
'Kecamatan',
|
||||
'Desa'
|
||||
];
|
||||
}
|
||||
|
||||
public function title(): string
|
||||
{
|
||||
return mb_substr($this->subdistrict, 0, 31);
|
||||
}
|
||||
}
|
||||
|
||||
23
app/Exports/TaxationsExport.php
Normal file
23
app/Exports/TaxationsExport.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use App\Models\Tax;
|
||||
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
|
||||
|
||||
class TaxationsExport implements WithMultipleSheets
|
||||
{
|
||||
public function sheets(): array
|
||||
{
|
||||
$sheets = [];
|
||||
|
||||
// Ambil semua subdistrict unik
|
||||
$subdistricts = Tax::select('subdistrict')->distinct()->pluck('subdistrict');
|
||||
|
||||
foreach ($subdistricts as $subdistrict) {
|
||||
$sheets[] = new TaxSubdistrictSheetExport($subdistrict);
|
||||
}
|
||||
|
||||
return $sheets;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\BigdataResumeResource;
|
||||
use App\Models\BigdataResume;
|
||||
use App\Models\DataSetting;
|
||||
use App\Models\SpatialPlanning;
|
||||
use App\Models\PbgTaskPayment;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -23,14 +25,16 @@ class BigDataResumeController extends Controller
|
||||
{
|
||||
try{
|
||||
$filterDate = $request->get("filterByDate");
|
||||
$type = trim($request->get("type"));
|
||||
|
||||
if (!$filterDate || $filterDate === "latest") {
|
||||
$big_data_resume = BigdataResume::where('year', now()->year)->latest()->first();
|
||||
$big_data_resume = BigdataResume::where('resume_type', $type)->latest()->first();
|
||||
if (!$big_data_resume) {
|
||||
return $this->response_empty_resume();
|
||||
}
|
||||
} else {
|
||||
$big_data_resume = BigdataResume::whereDate('created_at', $filterDate)
|
||||
->where('resume_type', $type)
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
@@ -40,23 +44,28 @@ class BigDataResumeController extends Controller
|
||||
}
|
||||
|
||||
$data_settings = DataSetting::all();
|
||||
if($data_settings->isEmpty()){
|
||||
return response()->json(['message' => 'No data setting found']);
|
||||
$target_pad = 0;
|
||||
if($data_settings->where('key', 'TARGET_PAD')->first()){
|
||||
$target_pad = floatval($data_settings->where('key', 'TARGET_PAD')->first()->value ?? 0);
|
||||
}
|
||||
|
||||
function cleanNumber($value) {
|
||||
return floatval(str_replace('.', '', $value));
|
||||
}
|
||||
$realisasi_terbit_pbg_sum = $big_data_resume->issuance_realization_pbg_sum;
|
||||
$realisasi_terbit_pbg_count = $big_data_resume->issuance_realization_pbg_count;
|
||||
$menunggu_klik_dpmptsp_sum = $big_data_resume->waiting_click_dpmptsp_sum;
|
||||
$menunggu_klik_dpmptsp_count = $big_data_resume->waiting_click_dpmptsp_count;
|
||||
$proses_dinas_teknis_sum = $big_data_resume->process_in_technical_office_sum;
|
||||
$proses_dinas_teknis_count = $big_data_resume->process_in_technical_office_count;
|
||||
|
||||
$target_pad = floatval(optional($data_settings->where('key', 'TARGET_PAD')->first())->value);
|
||||
$realisasi_terbit_pbg_sum = cleanNumber(optional($data_settings->where('key', 'REALISASI_TERBIT_PBG_SUM')->first())->value);
|
||||
$realisasi_terbit_pbg_count = cleanNumber(optional($data_settings->where('key', 'REALISASI_TERBIT_PBG_COUNT')->first())->value);
|
||||
$menunggu_klik_dpmptsp_sum = cleanNumber(optional($data_settings->where('key', 'MENUNGGU_KLIK_DPMPTSP_SUM')->first())->value);
|
||||
$menunggu_klik_dpmptsp_count = cleanNumber(optional($data_settings->where('key', 'MENUNGGU_KLIK_DPMPTSP_COUNT')->first())->value);
|
||||
$proses_dinas_teknis_sum = cleanNumber(optional($data_settings->where('key', 'PROSES_DINAS_TEKNIS_SUM')->first())->value);
|
||||
$proses_dinas_teknis_count = cleanNumber(optional($data_settings->where('key', 'PROSES_DINAS_TEKNIS_COUNT')->first())->value);
|
||||
// Get real-time spatial planning data using new calculation formula
|
||||
$spatialData = $this->getSpatialPlanningData();
|
||||
$tata_ruang = $spatialData['sum'];
|
||||
$tata_ruang_count = $spatialData['count'];
|
||||
|
||||
// Get real-time PBG Task Payments data
|
||||
$pbgPaymentsData = $this->getPbgTaskPaymentsData();
|
||||
$pbg_task_payments_sum = $pbgPaymentsData['sum'];
|
||||
$pbg_task_payments_count = $pbgPaymentsData['count'];
|
||||
|
||||
$tata_ruang = $big_data_resume->spatial_sum;
|
||||
$kekurangan_potensi = $target_pad - $big_data_resume->potention_sum;
|
||||
|
||||
// percentage kekurangan potensi
|
||||
@@ -67,46 +76,62 @@ class BigDataResumeController extends Controller
|
||||
$total_potensi_percentage = $big_data_resume->potention_sum > 0 && $target_pad > 0
|
||||
? round(($big_data_resume->potention_sum / $target_pad) * 100, 2) : 0;
|
||||
|
||||
// percentage verified document
|
||||
$verified_percentage = $big_data_resume->verified_sum > 0 && $big_data_resume->potention_sum > 0
|
||||
// percentage verified document (verified_sum / potention_sum) - by value/amount
|
||||
$verified_percentage = $big_data_resume->potention_sum > 0 && $big_data_resume->verified_sum >= 0
|
||||
? round(($big_data_resume->verified_sum / $big_data_resume->potention_sum) * 100, 2) : 0;
|
||||
|
||||
// percentage non-verified document
|
||||
$non_verified_percentage = $big_data_resume->non_verified_sum > 0 && $big_data_resume->potention_sum > 0
|
||||
// percentage non-verified document (non_verified_sum / potention_sum) - by value/amount
|
||||
$non_verified_percentage = $big_data_resume->potention_sum > 0 && $big_data_resume->non_verified_sum >= 0
|
||||
? round(($big_data_resume->non_verified_sum / $big_data_resume->potention_sum) * 100, 2) : 0;
|
||||
|
||||
// percentage business document
|
||||
$business_percentage = $big_data_resume->business_sum > 0 && $big_data_resume->non_verified_sum > 0
|
||||
// Alternative: percentage by count (if needed)
|
||||
// $verified_count_percentage = $big_data_resume->potention_count > 0
|
||||
// ? round(($big_data_resume->verified_count / $big_data_resume->potention_count) * 100, 2) : 0;
|
||||
// $non_verified_count_percentage = $big_data_resume->potention_count > 0
|
||||
// ? round(($big_data_resume->non_verified_count / $big_data_resume->potention_count) * 100, 2) : 0;
|
||||
|
||||
// percentage business document (business / non_verified)
|
||||
$business_percentage = $big_data_resume->non_verified_sum > 0 && $big_data_resume->business_sum >= 0
|
||||
? round(($big_data_resume->business_sum / $big_data_resume->non_verified_sum) * 100, 2) : 0;
|
||||
|
||||
// percentage non-business document
|
||||
$non_business_percentage = $big_data_resume->non_business_sum > 0 && $big_data_resume->potention_sum > 0
|
||||
? round(($big_data_resume->non_business_sum / $big_data_resume->potention_sum) * 100, 2) : 0;
|
||||
// percentage non-business document (non_business / non_verified)
|
||||
$non_business_percentage = $big_data_resume->non_verified_sum > 0 && $big_data_resume->non_business_sum >= 0
|
||||
? round(($big_data_resume->non_business_sum / $big_data_resume->non_verified_sum) * 100, 2) : 0;
|
||||
|
||||
// percentage tata ruang
|
||||
$tata_ruang_percentage = $tata_ruang > 0 && $big_data_resume->potention_sum > 0
|
||||
// percentage tata ruang (spatial / potention)
|
||||
$tata_ruang_percentage = $big_data_resume->potention_sum > 0 && $tata_ruang >= 0
|
||||
? round(($tata_ruang / $big_data_resume->potention_sum) * 100, 2) : 0;
|
||||
|
||||
// percentage realisasi terbit pbg
|
||||
$realisasi_terbit_percentage = $big_data_resume->verified_sum > 0 && $realisasi_terbit_pbg_sum > 0
|
||||
// percentage realisasi terbit pbg (issuance / verified)
|
||||
$realisasi_terbit_percentage = $big_data_resume->verified_sum > 0 && $realisasi_terbit_pbg_sum >= 0
|
||||
? round(($realisasi_terbit_pbg_sum / $big_data_resume->verified_sum) * 100, 2) : 0;
|
||||
|
||||
// percentage menunggu klik dpmptsp
|
||||
$menunggu_klik_dpmptsp_percentage = $big_data_resume->verified_sum > 0 && $menunggu_klik_dpmptsp_sum > 0
|
||||
// percentage menunggu klik dpmptsp (waiting / verified)
|
||||
$menunggu_klik_dpmptsp_percentage = $big_data_resume->verified_sum > 0 && $menunggu_klik_dpmptsp_sum >= 0
|
||||
? round(($menunggu_klik_dpmptsp_sum / $big_data_resume->verified_sum) * 100, 2) : 0;
|
||||
|
||||
// percentage proses_dinas_teknis
|
||||
$proses_dinas_teknis_percentage = $big_data_resume->verified_sum > 0 && $proses_dinas_teknis_sum > 0
|
||||
// percentage proses_dinas_teknis (process / verified)
|
||||
$proses_dinas_teknis_percentage = $big_data_resume->verified_sum > 0 && $proses_dinas_teknis_sum >= 0
|
||||
? round(($proses_dinas_teknis_sum / $big_data_resume->verified_sum) * 100, 2) : 0;
|
||||
|
||||
// percentage pbg_task_payments (payments / verified)
|
||||
$pbg_task_payments_percentage = $realisasi_terbit_pbg_sum > 0 && $pbg_task_payments_sum >= 0
|
||||
? round(($pbg_task_payments_sum / $realisasi_terbit_pbg_sum) * 100, 2) : 0;
|
||||
|
||||
$business_rab_count = $big_data_resume->business_rab_count;
|
||||
$business_krk_count = $big_data_resume->business_krk_count;
|
||||
$non_business_rab_count = $big_data_resume->non_business_rab_count;
|
||||
$non_business_krk_count = $big_data_resume->non_business_krk_count;
|
||||
$business_dlh_count = $big_data_resume->business_dlh_count;
|
||||
|
||||
$result = [
|
||||
'target_pad' => [
|
||||
'sum' => $target_pad,
|
||||
'percentage' => 100,
|
||||
],
|
||||
'tata_ruang' => [
|
||||
'sum' => $big_data_resume->spatial_sum,
|
||||
'count' => $big_data_resume->spatial_count,
|
||||
'sum' => $tata_ruang,
|
||||
'count' => $tata_ruang_count,
|
||||
'percentage' => $tata_ruang_percentage,
|
||||
],
|
||||
'kekurangan_potensi' => [
|
||||
@@ -152,6 +177,16 @@ class BigDataResumeController extends Controller
|
||||
'sum' => $proses_dinas_teknis_sum,
|
||||
'count' => $proses_dinas_teknis_count,
|
||||
'percentage' => $proses_dinas_teknis_percentage
|
||||
],
|
||||
'business_rab_count' => $business_rab_count,
|
||||
'business_krk_count' => $business_krk_count,
|
||||
'non_business_rab_count' => $non_business_rab_count,
|
||||
'non_business_krk_count' => $non_business_krk_count,
|
||||
'business_dlh_count' => $business_dlh_count,
|
||||
'pbg_task_payments' => [
|
||||
'sum' => (float) $pbg_task_payments_sum,
|
||||
'count' => $pbg_task_payments_count,
|
||||
'percentage' => $pbg_task_payments_percentage
|
||||
]
|
||||
];
|
||||
return response()->json($result);
|
||||
@@ -321,9 +356,15 @@ class BigDataResumeController extends Controller
|
||||
return $pdf->download('laporan-pimpinan.pdf');
|
||||
}
|
||||
private function response_empty_resume(){
|
||||
$data_settings = DataSetting::all();
|
||||
$target_pad = 0;
|
||||
if($data_settings->where('key', 'TARGET_PAD')->first()){
|
||||
$target_pad = floatval($data_settings->where('key', 'TARGET_PAD')->first()->value ?? 0);
|
||||
}
|
||||
|
||||
$result = [
|
||||
'target_pad' => [
|
||||
'sum' => 0,
|
||||
'sum' => $target_pad,
|
||||
'percentage' => 100,
|
||||
],
|
||||
'tata_ruang' => [
|
||||
@@ -373,9 +414,97 @@ class BigDataResumeController extends Controller
|
||||
'sum' => 0,
|
||||
'count' => 0,
|
||||
'percentage' => 0
|
||||
],
|
||||
'pbg_task_payments' => [
|
||||
'sum' => 0,
|
||||
'count' => 0,
|
||||
'percentage' => 0
|
||||
]
|
||||
];
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get spatial planning data using new calculation formula
|
||||
*/
|
||||
private function getSpatialPlanningData(): array
|
||||
{
|
||||
try {
|
||||
// Get spatial plannings that are not yet issued (is_terbit = false) and have valid data
|
||||
$spatialPlannings = SpatialPlanning::where('land_area', '>', 0)
|
||||
->where('site_bcr', '>', 0)
|
||||
->where('is_terbit', false)
|
||||
->get();
|
||||
|
||||
$totalSum = 0;
|
||||
$businessCount = 0;
|
||||
$nonBusinessCount = 0;
|
||||
|
||||
foreach ($spatialPlannings as $spatialPlanning) {
|
||||
// Use new calculation formula: LUAS LAHAN × BCR × HARGA SATUAN
|
||||
$calculatedAmount = $spatialPlanning->calculated_retribution;
|
||||
$totalSum += $calculatedAmount;
|
||||
|
||||
// Count business types
|
||||
if ($spatialPlanning->is_business_type) {
|
||||
$businessCount++;
|
||||
} else {
|
||||
$nonBusinessCount++;
|
||||
}
|
||||
}
|
||||
|
||||
Log::info("Real-time Spatial Planning Data (is_terbit = false only)", [
|
||||
'total_records' => $spatialPlannings->count(),
|
||||
'business_count' => $businessCount,
|
||||
'non_business_count' => $nonBusinessCount,
|
||||
'total_sum' => $totalSum,
|
||||
'filtered_by' => 'is_terbit = false'
|
||||
]);
|
||||
|
||||
return [
|
||||
'count' => $spatialPlannings->count(),
|
||||
'sum' => (float) $totalSum,
|
||||
'business_count' => $businessCount,
|
||||
'non_business_count' => $nonBusinessCount,
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error getting spatial planning data", ['error' => $e->getMessage()]);
|
||||
return [
|
||||
'count' => 0,
|
||||
'sum' => 0.0,
|
||||
'business_count' => 0,
|
||||
'non_business_count' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PBG Task Payments data from database
|
||||
*/
|
||||
private function getPbgTaskPaymentsData(): array
|
||||
{
|
||||
try {
|
||||
// Get sum and count from PbgTaskPayment model
|
||||
$stats = PbgTaskPayment::whereNotNull('payment_date_raw')
|
||||
->whereNotNull('retribution_total_pad')
|
||||
->whereYear('payment_date_raw', date('Y'))
|
||||
->selectRaw('SUM(retribution_total_pad) as total_sum, COUNT(*) as total_count')
|
||||
->first();
|
||||
|
||||
$totalSum = $stats->total_sum ?? 0;
|
||||
$totalCount = $stats->total_count ?? 0;
|
||||
|
||||
return [
|
||||
'sum' => (float) $totalSum,
|
||||
'count' => (int) $totalCount,
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error getting PBG task payments data", ['error' => $e->getMessage()]);
|
||||
return [
|
||||
'sum' => 0.0,
|
||||
'count' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Http\Resources\CustomersResource;
|
||||
use App\Imports\CustomersImport;
|
||||
use App\Models\Customer;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
|
||||
class CustomersController extends Controller
|
||||
@@ -120,7 +121,7 @@ class CustomersController extends Controller
|
||||
'message' => 'File uploaded successfully',
|
||||
]);
|
||||
}catch(\Exception $e){
|
||||
\Log::info($e->getMessage());
|
||||
Log::info($e->getMessage());
|
||||
return response()->json([
|
||||
'error' => 'Failed to upload file',
|
||||
'message' => $e->getMessage()
|
||||
|
||||
@@ -23,19 +23,25 @@ class GrowthReportAPIController extends Controller
|
||||
$defaultEnd = $today;
|
||||
|
||||
// Use request values if provided, else use defaults
|
||||
$startDate = $request->input('start_date', $defaultStart->toDateString());
|
||||
$endDate = $request->input('end_date', $defaultEnd->toDateString());
|
||||
// $startDate = $request->input('start_date', $defaultStart->toDateString());
|
||||
// $endDate = $request->input('end_date', $defaultEnd->toDateString());
|
||||
|
||||
// Optional year filter (used if specified)
|
||||
$year = $request->input('year', now()->year);
|
||||
|
||||
// $query = BigdataResume::selectRaw("
|
||||
// DATE(created_at) as date,
|
||||
// SUM(potention_sum) as potention_sum,
|
||||
// SUM(verified_sum) as verified_sum,
|
||||
// SUM(non_verified_sum) as non_verified_sum
|
||||
// ")
|
||||
// ->whereBetween('created_at', [$startDate, $endDate]);
|
||||
$query = BigdataResume::selectRaw("
|
||||
DATE(created_at) as date,
|
||||
SUM(potention_sum) as potention_sum,
|
||||
SUM(verified_sum) as verified_sum,
|
||||
SUM(non_verified_sum) as non_verified_sum
|
||||
")
|
||||
->whereBetween('created_at', [$startDate, $endDate]);
|
||||
");
|
||||
|
||||
$query->whereNotNull('year')
|
||||
->where('year', '!=', 'all');
|
||||
|
||||
@@ -8,6 +8,8 @@ use App\Models\Customer;
|
||||
use App\Models\SpatialPlanning;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\TourismBasedKBLI;
|
||||
use App\Models\Tax;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class LackOfPotentialController extends Controller
|
||||
{
|
||||
@@ -17,13 +19,28 @@ class LackOfPotentialController extends Controller
|
||||
$total_reklame = Advertisement::count();
|
||||
$total_pdam = Customer::count();
|
||||
$total_tata_ruang = SpatialPlanning::count();
|
||||
$total_tata_ruang_usaha = SpatialPlanning::where('building_function','like', '%usaha%')->count();
|
||||
$total_tata_ruang_non_usaha = SpatialPlanning::where('building_function','not like', '%usaha%')->count();
|
||||
$data_report_tourism = TourismBasedKBLI::all();
|
||||
$data_pajak_reklame = Tax::where('tax_code','Reklame')->distinct('business_name')->count();
|
||||
$data_pajak_restoran = Tax::where('tax_code','Restoran')->distinct('business_name')->count();
|
||||
$data_pajak_hiburan = Tax::where('tax_code','Hiburan')->distinct('business_name')->count();
|
||||
$data_pajak_hotel = Tax::where('tax_code','Hotel')->distinct('business_name')->count();
|
||||
$data_pajak_parkir = Tax::where('tax_code','Parkir')->distinct('business_name')->count();
|
||||
|
||||
return response()->json([
|
||||
'total_reklame' => $total_reklame,
|
||||
'total_pdam' => $total_pdam,
|
||||
'total_tata_ruang' => $total_tata_ruang,
|
||||
'total_tata_ruang_usaha' => $total_tata_ruang_usaha,
|
||||
'total_tata_ruang_non_usaha' => $total_tata_ruang_non_usaha,
|
||||
'data_report' => $data_report_tourism,
|
||||
'data_pajak_reklame' => $data_pajak_reklame,
|
||||
'data_pajak_restoran' => $data_pajak_restoran,
|
||||
'data_pajak_hiburan' => $data_pajak_hiburan,
|
||||
'data_pajak_hotel' => $data_pajak_hotel,
|
||||
'data_pajak_parkir' => $data_pajak_parkir,
|
||||
'tata_ruang' => $this->getSpatialPlanningData()
|
||||
], 200);
|
||||
}catch(\Exception $e){
|
||||
return response()->json([
|
||||
@@ -31,4 +48,63 @@ class LackOfPotentialController extends Controller
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
private function getSpatialPlanningData(): array
|
||||
{
|
||||
try {
|
||||
// Get spatial plannings that are not yet issued (is_terbit = false) and have valid data
|
||||
$spatialPlannings = SpatialPlanning::where('land_area', '>', 0)
|
||||
->where('site_bcr', '>', 0)
|
||||
->where('is_terbit', false)
|
||||
->get();
|
||||
|
||||
$totalSum = 0;
|
||||
$businessCount = 0;
|
||||
$nonBusinessCount = 0;
|
||||
$businessSum = 0;
|
||||
$nonBusinessSum = 0;
|
||||
|
||||
foreach ($spatialPlannings as $spatialPlanning) {
|
||||
// Use new calculation formula: LUAS LAHAN × BCR × HARGA SATUAN
|
||||
$calculatedAmount = $spatialPlanning->calculated_retribution;
|
||||
$totalSum += $calculatedAmount;
|
||||
|
||||
// Count business types
|
||||
if ($spatialPlanning->is_business_type) {
|
||||
$businessCount++;
|
||||
$businessSum += $calculatedAmount;
|
||||
} else {
|
||||
$nonBusinessCount++;
|
||||
$nonBusinessSum += $calculatedAmount;
|
||||
}
|
||||
}
|
||||
|
||||
Log::info("Real-time Spatial Planning Data (is_terbit = false only)", [
|
||||
'total_records' => $spatialPlannings->count(),
|
||||
'business_count' => $businessCount,
|
||||
'non_business_count' => $nonBusinessCount,
|
||||
'total_sum' => $totalSum,
|
||||
'filtered_by' => 'is_terbit = false'
|
||||
]);
|
||||
|
||||
return [
|
||||
'count' => $spatialPlannings->count(),
|
||||
'sum' => (float) $totalSum,
|
||||
'business_count' => $businessCount,
|
||||
'non_business_count' => $nonBusinessCount,
|
||||
'business_sum' => (float) $businessSum,
|
||||
'non_business_sum' => (float) $nonBusinessSum,
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error getting spatial planning data", ['error' => $e->getMessage()]);
|
||||
return [
|
||||
'count' => 0,
|
||||
'sum' => 0.0,
|
||||
'business_count' => 0,
|
||||
'non_business_count' => 0,
|
||||
'business_sum' => 0.0,
|
||||
'non_business_sum' => 0.0,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,39 +132,48 @@ class PbgTaskController extends Controller
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'owner_name' => 'required|string|max:255',
|
||||
'name' => 'nullable|string|max:255',
|
||||
'owner_name' => 'nullable|string|max:255',
|
||||
'application_type' => ['nullable', new Enum(PbgTaskApplicationTypes::class)],
|
||||
'condition' => 'required|string|max:255',
|
||||
'registration_number' => 'required|string|max:255',
|
||||
'document_number' => 'required|string|max:255',
|
||||
'condition' => 'nullable|string|max:255',
|
||||
'registration_number' => 'nullable|string|max:255',
|
||||
'document_number' => 'nullable|string|max:255',
|
||||
'status' => ['nullable', new Enum(PbgTaskStatus::class)],
|
||||
'address' => 'required|string|max:255',
|
||||
'address' => 'nullable|string|max:255',
|
||||
'slf_status_name' => 'nullable|string|max:255',
|
||||
'function_type' => 'required|string|max:255',
|
||||
'consultation_type' => 'required|string|max:255',
|
||||
'due_date' => 'nullable|date|after_or_equal:today',
|
||||
'function_type' => 'nullable|string|max:255',
|
||||
'consultation_type' => 'nullable|string|max:255',
|
||||
'due_date' => 'nullable|date',
|
||||
'is_valid' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$statusLabel = $validated['status'] !== null ? PbgTaskStatus::getLabel($validated['status']) : null;
|
||||
$applicationLabel = $validated['application_type'] !== null ? PbgTaskApplicationTypes::getLabel($validated['application_type']) : null;
|
||||
|
||||
$pbg_task->update([
|
||||
'name' => $validated['name'],
|
||||
'owner_name' => $validated['owner_name'],
|
||||
'application_type' => $validated['application_type'],
|
||||
'application_type_name' => $applicationLabel, // Automatically set application_type_name
|
||||
'condition' => $validated['condition'],
|
||||
'registration_number' => $validated['registration_number'],
|
||||
'document_number' => $validated['document_number'],
|
||||
'status' => $validated['status'],
|
||||
'status_name' => $statusLabel, // Automatically set status_name
|
||||
'address' => $validated['address'],
|
||||
'slf_status_name' => $validated['slf_status_name'],
|
||||
'function_type' => $validated['function_type'],
|
||||
'consultation_type' => $validated['consultation_type'],
|
||||
'due_date' => $validated['due_date'],
|
||||
]);
|
||||
// Prepare update data - only include fields that are actually provided
|
||||
$updateData = [];
|
||||
|
||||
foreach ($validated as $key => $value) {
|
||||
if ($value !== null || $request->has($key)) {
|
||||
$updateData[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle special cases for labels
|
||||
if (isset($updateData['status'])) {
|
||||
$updateData['status_name'] = $statusLabel;
|
||||
}
|
||||
|
||||
if (isset($updateData['application_type'])) {
|
||||
$updateData['application_type_name'] = $applicationLabel;
|
||||
}
|
||||
|
||||
// Handle is_valid specifically
|
||||
if ($request->has('is_valid')) {
|
||||
$updateData['is_valid'] = $validated['is_valid'];
|
||||
}
|
||||
|
||||
$pbg_task->update($updateData);
|
||||
return response()->json([
|
||||
"success"=> true,
|
||||
"message"=> "Data berhasil diubah",
|
||||
|
||||
@@ -11,8 +11,9 @@ use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Log;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use App\Enums\PbgTaskStatus;
|
||||
|
||||
class RequestAssignmentController extends Controller
|
||||
{
|
||||
@@ -21,52 +22,365 @@ class RequestAssignmentController extends Controller
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = PbgTask::with([
|
||||
// Build base query for counting (without relationships to avoid duplicates)
|
||||
$baseQuery = PbgTask::query();
|
||||
|
||||
// Always filter only valid data (is_valid = true)
|
||||
$baseQuery->where('is_valid', true);
|
||||
|
||||
// Apply year filter if provided (to match BigdataResume behavior)
|
||||
if ($request->has('year') && !empty($request->get('year'))) {
|
||||
$year = $request->get('year');
|
||||
$baseQuery->where('due_date', '>=', $year.'-02-01');
|
||||
Log::info('RequestAssignmentController year filter applied', ['year' => $year]);
|
||||
}
|
||||
|
||||
// Get filter value, default to 'all' if not provided or empty
|
||||
$filter = $request->has('filter') && !empty($request->get('filter'))
|
||||
? strtolower(trim($request->get('filter')))
|
||||
: 'all';
|
||||
|
||||
// Log filter for debugging
|
||||
Log::info('RequestAssignmentController filter applied', ['filter' => $filter, 'original' => $request->get('filter')]);
|
||||
|
||||
// Apply filters to base query using single consolidated method
|
||||
$this->applyFilter($baseQuery, $filter);
|
||||
|
||||
// Get accurate count from base query (without relationships)
|
||||
$accurateCount = $baseQuery->count();
|
||||
|
||||
// Clone the base query for data fetching with relationships
|
||||
$dataQuery = clone $baseQuery;
|
||||
|
||||
$dataQuery->with([
|
||||
'attachments' => function ($q) {
|
||||
$q->whereIn('pbg_type', ['berita_acara', 'bukti_bayar']);
|
||||
},
|
||||
'googleSheet'
|
||||
'pbg_task_retributions',
|
||||
'pbg_task_detail',
|
||||
'pbg_status'
|
||||
])->orderBy('id', 'desc');
|
||||
|
||||
if ($request->has('filter') && !empty($request->get('filter'))) {
|
||||
$filter = strtolower($request->get('filter'));
|
||||
// Log final query count for debugging
|
||||
Log::info('RequestAssignmentController final result', [
|
||||
'filter' => $filter,
|
||||
'search' => $request->get('search'),
|
||||
'year' => $request->get('year'),
|
||||
'accurate_count' => $accurateCount,
|
||||
'request_url' => $request->fullUrl(),
|
||||
'all_params' => $request->all()
|
||||
]);
|
||||
|
||||
// Cross-validation with BigdataResume logic (for debugging consistency)
|
||||
if ($filter !== 'all' && $request->has('year') && !empty($request->get('year'))) {
|
||||
$this->validateConsistencyWithBigdataResume($filter, $request->get('year'), $accurateCount);
|
||||
}
|
||||
|
||||
// Apply search to data query
|
||||
if ($request->has('search') && !empty($request->get("search"))) {
|
||||
$this->applySearch($dataQuery, $request->get('search'));
|
||||
}
|
||||
|
||||
// Additional logging for potention filter
|
||||
if ($filter === 'potention') {
|
||||
$rejectedCount = PbgTask::whereIn('status', PbgTaskStatus::getRejected())->count();
|
||||
Log::info('Potention filter details', [
|
||||
'potention_count' => $accurateCount,
|
||||
'rejected_count' => $rejectedCount,
|
||||
'total_all_records' => PbgTask::count(),
|
||||
'note' => 'Potention filter excludes rejected data'
|
||||
]);
|
||||
}
|
||||
|
||||
// Also log to console for immediate debugging
|
||||
if ($filter !== 'all') {
|
||||
error_log('RequestAssignment Filter Debug: ' . $filter . ' -> Count: ' . $accurateCount);
|
||||
}
|
||||
|
||||
// Get paginated results with relationships
|
||||
$paginatedResults = $dataQuery->paginate();
|
||||
|
||||
// Append query parameters to pagination
|
||||
$paginatedResults->appends($request->query());
|
||||
|
||||
return RequestAssignmentResouce::collection($paginatedResults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filter logic to the query
|
||||
*/
|
||||
private function applyFilter($query, string $filter)
|
||||
{
|
||||
switch ($filter) {
|
||||
case 'all':
|
||||
// No additional filters, just return all valid records
|
||||
break;
|
||||
case 'non-business':
|
||||
$query->whereRaw("LOWER(function_type) != ?", ['sebagai tempat usaha'])->orWhereNull('function_type');
|
||||
// Non-business: function_type NOT LIKE usaha AND (unit IS NULL OR unit <= 1)
|
||||
$query->where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
$q2->where(function ($q3) {
|
||||
$q3->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
|
||||
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
->orWhereNull('function_type');
|
||||
})
|
||||
->whereIn("status", PbgTaskStatus::getNonVerified())
|
||||
// Additional condition: unit IS NULL OR unit <= 1
|
||||
->where(function ($q3) {
|
||||
$q3->whereDoesntHave('pbg_task_detail', function ($q4) {
|
||||
$q4->where('unit', '>', 1);
|
||||
})
|
||||
->orWhereDoesntHave('pbg_task_detail');
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
case 'business':
|
||||
$query->whereRaw("LOWER(function_type) = ?", ['sebagai tempat usaha']);
|
||||
// Business: function_type LIKE usaha OR (non-business with unit > 1)
|
||||
$query->where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
// Traditional business: function_type LIKE usaha
|
||||
$q2->where(function ($q3) {
|
||||
$q3->whereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%fungsi usaha%'])
|
||||
->orWhereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
// OR non-business with unit > 1 (becomes business)
|
||||
->orWhere(function ($q3) {
|
||||
$q3->where(function ($q4) {
|
||||
$q4->where(function ($q5) {
|
||||
$q5->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
|
||||
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
->orWhereNull('function_type');
|
||||
})
|
||||
->whereHas('pbg_task_detail', function ($q4) {
|
||||
$q4->where('unit', '>', 1);
|
||||
});
|
||||
});
|
||||
})
|
||||
->whereIn("status", PbgTaskStatus::getNonVerified());
|
||||
});
|
||||
break;
|
||||
|
||||
case 'verified':
|
||||
$query->whereHas('googleSheet', function ($q) {
|
||||
$q->whereRaw("LOWER(status_verifikasi) = ?", ['selesai verifikasi']);
|
||||
});
|
||||
// Match BigdataResume verified logic exactly
|
||||
$query->whereIn("status", PbgTaskStatus::getVerified());
|
||||
break;
|
||||
|
||||
case 'non-verified':
|
||||
// Match BigdataResume non-verified logic exactly
|
||||
$query->whereIn("status", PbgTaskStatus::getNonVerified());
|
||||
break;
|
||||
|
||||
case 'potention':
|
||||
// Match BigdataResume potention logic exactly
|
||||
$query->whereIn("status", PbgTaskStatus::getPotention());
|
||||
break;
|
||||
|
||||
case 'issuance-realization-pbg':
|
||||
// Match BigdataResume issuance realization logic exactly
|
||||
$query->whereIn("status", PbgTaskStatus::getIssuanceRealizationPbg());
|
||||
break;
|
||||
|
||||
case 'process-in-technical-office':
|
||||
// Match BigdataResume process in technical office logic exactly
|
||||
$query->whereIn("status", PbgTaskStatus::getProcessInTechnicalOffice());
|
||||
break;
|
||||
|
||||
case 'waiting-click-dpmptsp':
|
||||
// Match BigdataResume waiting click DPMPTSP logic exactly
|
||||
$query->whereIn("status", PbgTaskStatus::getWaitingClickDpmptsp());
|
||||
break;
|
||||
|
||||
case 'non-business-rab':
|
||||
// Non-business tasks: function_type NOT LIKE usaha AND (unit IS NULL OR unit <= 1)
|
||||
$query->where(function ($q) {
|
||||
$q->whereDoesntHave('googleSheet')
|
||||
->orWhereHas('googleSheet', function ($q2) {
|
||||
$q2->whereRaw("LOWER(status_verifikasi) != ?", ['selesai verifikasi'])->orWhereNull('status_verifikasi');
|
||||
$q->where(function ($q2) {
|
||||
$q2->where(function ($q3) {
|
||||
$q3->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
|
||||
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
->orWhereNull('function_type');
|
||||
})
|
||||
->whereIn("status", PbgTaskStatus::getNonVerified())
|
||||
// Additional condition: unit IS NULL OR unit <= 1
|
||||
->where(function ($q3) {
|
||||
$q3->whereDoesntHave('pbg_task_detail', function ($q4) {
|
||||
$q4->where('unit', '>', 1);
|
||||
})
|
||||
->orWhereDoesntHave('pbg_task_detail');
|
||||
});
|
||||
})
|
||||
->whereExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('pbg_task_detail_data_lists')
|
||||
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
|
||||
->where('pbg_task_detail_data_lists.data_type', 3)
|
||||
->where('pbg_task_detail_data_lists.status', '!=', 1);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'non-business-krk':
|
||||
// Non-business tasks: function_type NOT LIKE usaha AND (unit IS NULL OR unit <= 1)
|
||||
$query->where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
$q2->where(function ($q3) {
|
||||
$q3->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
|
||||
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
->orWhereNull('function_type');
|
||||
})
|
||||
->whereIn("status", PbgTaskStatus::getNonVerified())
|
||||
// Additional condition: unit IS NULL OR unit <= 1
|
||||
->where(function ($q3) {
|
||||
$q3->whereDoesntHave('pbg_task_detail', function ($q4) {
|
||||
$q4->where('unit', '>', 1);
|
||||
})
|
||||
->orWhereDoesntHave('pbg_task_detail');
|
||||
});
|
||||
})
|
||||
->whereExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('pbg_task_detail_data_lists')
|
||||
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
|
||||
->where('pbg_task_detail_data_lists.data_type', 2)
|
||||
->where('pbg_task_detail_data_lists.status', '!=', 1);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'business-rab':
|
||||
// Business tasks: function_type LIKE usaha OR (non-business with unit > 1)
|
||||
$query->where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
// Traditional business: function_type LIKE usaha
|
||||
$q2->where(function ($q3) {
|
||||
$q3->whereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%fungsi usaha%'])
|
||||
->orWhereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
// OR non-business with unit > 1 (becomes business)
|
||||
->orWhere(function ($q3) {
|
||||
$q3->where(function ($q4) {
|
||||
$q4->where(function ($q5) {
|
||||
$q5->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
|
||||
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
->orWhereNull('function_type');
|
||||
})
|
||||
->whereHas('pbg_task_detail', function ($q4) {
|
||||
$q4->where('unit', '>', 1);
|
||||
});
|
||||
});
|
||||
})
|
||||
->whereIn("status", PbgTaskStatus::getNonVerified());
|
||||
})
|
||||
->whereExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('pbg_task_detail_data_lists')
|
||||
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
|
||||
->where('pbg_task_detail_data_lists.data_type', 3)
|
||||
->where('pbg_task_detail_data_lists.status', '!=', 1);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'business-krk':
|
||||
// Business tasks: function_type LIKE usaha OR (non-business with unit > 1)
|
||||
$query->where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
// Traditional business: function_type LIKE usaha
|
||||
$q2->where(function ($q3) {
|
||||
$q3->whereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%fungsi usaha%'])
|
||||
->orWhereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
// OR non-business with unit > 1 (becomes business)
|
||||
->orWhere(function ($q3) {
|
||||
$q3->where(function ($q4) {
|
||||
$q4->where(function ($q5) {
|
||||
$q5->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
|
||||
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
->orWhereNull('function_type');
|
||||
})
|
||||
->whereHas('pbg_task_detail', function ($q4) {
|
||||
$q4->where('unit', '>', 1);
|
||||
});
|
||||
});
|
||||
})
|
||||
->whereIn("status", PbgTaskStatus::getNonVerified());
|
||||
})
|
||||
->whereExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('pbg_task_detail_data_lists')
|
||||
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
|
||||
->where('pbg_task_detail_data_lists.data_type', 2)
|
||||
->where('pbg_task_detail_data_lists.status', '!=', 1);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'business-dlh':
|
||||
// Business tasks: function_type LIKE usaha OR (non-business with unit > 1)
|
||||
$query->where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
// Traditional business: function_type LIKE usaha
|
||||
$q2->where(function ($q3) {
|
||||
$q3->whereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%fungsi usaha%'])
|
||||
->orWhereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
// OR non-business with unit > 1 (becomes business)
|
||||
->orWhere(function ($q3) {
|
||||
$q3->where(function ($q4) {
|
||||
$q4->where(function ($q5) {
|
||||
$q5->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
|
||||
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
->orWhereNull('function_type');
|
||||
})
|
||||
->whereHas('pbg_task_details', function ($q4) {
|
||||
$q4->where('unit', '>', 1);
|
||||
});
|
||||
});
|
||||
})
|
||||
->whereIn("status", PbgTaskStatus::getNonVerified());
|
||||
})
|
||||
->whereExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('pbg_task_detail_data_lists')
|
||||
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
|
||||
->where('pbg_task_detail_data_lists.data_type', 5)
|
||||
->where('pbg_task_detail_data_lists.status', '!=', 1);
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
// Log unrecognized filter for debugging
|
||||
Log::warning('Unrecognized filter value', ['filter' => $filter]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->has('search') && !empty($request->get("search"))) {
|
||||
$search = $request->get('search');
|
||||
/**
|
||||
* Apply search logic to the query
|
||||
*/
|
||||
private function applySearch($query, string $search)
|
||||
{
|
||||
// Search in pbg_task columns
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'LIKE', "%$search%")
|
||||
->orWhere('registration_number', 'LIKE', "%$search%")
|
||||
->orWhere('document_number', 'LIKE', "%$search%");
|
||||
->orWhere('owner_name', 'LIKE', "%$search%")
|
||||
->orWhere('address', 'LIKE', "%$search%");
|
||||
});
|
||||
}
|
||||
|
||||
return RequestAssignmentResouce::collection($query->paginate());
|
||||
// If search term exists, also find UUIDs from name_building search
|
||||
$namesBuildingUuids = DB::table('pbg_task_details')
|
||||
->where('name_building', 'LIKE', "%$search%")
|
||||
->pluck('pbg_task_uid')
|
||||
->toArray();
|
||||
|
||||
// If we found matching name_building records, include them in the search
|
||||
if (!empty($namesBuildingUuids)) {
|
||||
$query->orWhereIn('uuid', $namesBuildingUuids);
|
||||
}
|
||||
}
|
||||
|
||||
public function report_payment_recaps(Request $request)
|
||||
@@ -158,4 +472,270 @@ class RequestAssignmentController extends Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate consistency with BigdataResume logic for debugging
|
||||
*/
|
||||
private function validateConsistencyWithBigdataResume(?string $filter, $year, int $actualCount)
|
||||
{
|
||||
try {
|
||||
// Validate input parameters
|
||||
if (empty($filter) || empty($year)) {
|
||||
Log::info('Skipping consistency validation - empty filter or year', [
|
||||
'filter' => $filter,
|
||||
'year' => $year
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert year to integer
|
||||
$year = (int) $year;
|
||||
if ($year <= 0) {
|
||||
Log::warning('Invalid year provided for consistency validation', ['year' => $year]);
|
||||
return;
|
||||
}
|
||||
|
||||
$bigdataResumeCount = null;
|
||||
|
||||
// Calculate expected count using BigdataResume logic
|
||||
switch ($filter) {
|
||||
case 'verified':
|
||||
$bigdataResumeCount = PbgTask::whereIn('status', PbgTaskStatus::getVerified())
|
||||
->where('is_valid', true)
|
||||
->whereYear('task_created_at', $year)
|
||||
->count();
|
||||
break;
|
||||
|
||||
case 'non-verified':
|
||||
$bigdataResumeCount = PbgTask::whereIn('status', PbgTaskStatus::getNonVerified())
|
||||
->where('is_valid', true)
|
||||
->whereYear('task_created_at', $year)
|
||||
->count();
|
||||
break;
|
||||
|
||||
case 'business':
|
||||
$bigdataResumeCount = PbgTask::where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
// Traditional business: function_type LIKE usaha
|
||||
$q2->where(function ($q3) {
|
||||
$q3->whereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%fungsi usaha%'])
|
||||
->orWhereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
// OR non-business with unit > 1 (becomes business)
|
||||
->orWhere(function ($q3) {
|
||||
$q3->where(function ($q4) {
|
||||
$q4->where(function ($q5) {
|
||||
$q5->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
|
||||
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
->orWhereNull('function_type');
|
||||
})
|
||||
->whereHas('pbg_task_details', function ($q4) {
|
||||
$q4->where('unit', '>', 1);
|
||||
});
|
||||
});
|
||||
})
|
||||
->whereIn("status", PbgTaskStatus::getNonVerified());
|
||||
})
|
||||
->where('is_valid', true)
|
||||
->whereYear('task_created_at', $year)
|
||||
->count();
|
||||
break;
|
||||
|
||||
case 'non-business':
|
||||
$bigdataResumeCount = PbgTask::where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
$q2->where(function ($q3) {
|
||||
$q3->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
|
||||
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
->orWhereNull('function_type');
|
||||
})
|
||||
->whereIn("status", PbgTaskStatus::getNonVerified())
|
||||
// Additional condition: unit IS NULL OR unit <= 1
|
||||
->where(function ($q3) {
|
||||
$q3->whereDoesntHave('pbg_task_details', function ($q4) {
|
||||
$q4->where('unit', '>', 1);
|
||||
})
|
||||
->orWhereDoesntHave('pbg_task_details');
|
||||
});
|
||||
})
|
||||
->where('is_valid', true)
|
||||
->whereYear('task_created_at', $year)
|
||||
->count();
|
||||
break;
|
||||
|
||||
case 'potention':
|
||||
$bigdataResumeCount = PbgTask::whereIn('status', PbgTaskStatus::getPotention())
|
||||
->where('is_valid', true)
|
||||
->whereYear('task_created_at', $year)
|
||||
->count();
|
||||
break;
|
||||
|
||||
case 'waiting-click-dpmptsp':
|
||||
$bigdataResumeCount = PbgTask::whereIn('status', PbgTaskStatus::getWaitingClickDpmptsp())
|
||||
->where('is_valid', true)
|
||||
->whereYear('task_created_at', $year)
|
||||
->count();
|
||||
break;
|
||||
|
||||
case 'issuance-realization-pbg':
|
||||
$bigdataResumeCount = PbgTask::whereIn('status', PbgTaskStatus::getIssuanceRealizationPbg())
|
||||
->where('is_valid', true)
|
||||
->whereYear('task_created_at', $year)
|
||||
->count();
|
||||
break;
|
||||
|
||||
case 'process-in-technical-office':
|
||||
$bigdataResumeCount = PbgTask::whereIn('status', PbgTaskStatus::getProcessInTechnicalOffice())
|
||||
->where('is_valid', true)
|
||||
->whereYear('task_created_at', $year)
|
||||
->count();
|
||||
break;
|
||||
|
||||
case 'non-business-rab':
|
||||
$bigdataResumeCount = PbgTask::where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
$q2->where(function ($q3) {
|
||||
$q3->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
|
||||
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
->orWhereNull('function_type');
|
||||
})
|
||||
->whereIn("status", PbgTaskStatus::getNonVerified());
|
||||
})
|
||||
->where('is_valid', true)
|
||||
->whereYear('task_created_at', $year)
|
||||
->whereExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('pbg_task_detail_data_lists')
|
||||
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
|
||||
->where('pbg_task_detail_data_lists.data_type', 3)
|
||||
->where('pbg_task_detail_data_lists.status', '!=', 1);
|
||||
})
|
||||
->count();
|
||||
break;
|
||||
|
||||
case 'non-business-krk':
|
||||
$bigdataResumeCount = PbgTask::where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
$q2->where(function ($q3) {
|
||||
$q3->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
|
||||
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
->orWhereNull('function_type');
|
||||
})
|
||||
->whereIn("status", PbgTaskStatus::getNonVerified());
|
||||
})
|
||||
->where('is_valid', true)
|
||||
->whereYear('task_created_at', $year)
|
||||
->whereExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('pbg_task_detail_data_lists')
|
||||
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
|
||||
->where('pbg_task_detail_data_lists.data_type', 2)
|
||||
->where('pbg_task_detail_data_lists.status', '!=', 1);
|
||||
})
|
||||
->count();
|
||||
break;
|
||||
|
||||
case 'business-rab':
|
||||
$bigdataResumeCount = PbgTask::where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
$q2->whereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%fungsi usaha%'])
|
||||
->orWhereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
->whereIn("status", PbgTaskStatus::getNonVerified());
|
||||
})
|
||||
->where('is_valid', true)
|
||||
->whereYear('task_created_at', $year)
|
||||
->whereExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('pbg_task_detail_data_lists')
|
||||
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
|
||||
->where('pbg_task_detail_data_lists.data_type', 3)
|
||||
->where('pbg_task_detail_data_lists.status', '!=', 1);
|
||||
})
|
||||
->count();
|
||||
break;
|
||||
|
||||
case 'business-krk':
|
||||
$bigdataResumeCount = PbgTask::where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
$q2->whereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%fungsi usaha%'])
|
||||
->orWhereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
->whereIn("status", PbgTaskStatus::getNonVerified());
|
||||
})
|
||||
->where('is_valid', true)
|
||||
->whereYear('task_created_at', $year)
|
||||
->whereExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('pbg_task_detail_data_lists')
|
||||
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
|
||||
->where('pbg_task_detail_data_lists.data_type', 2)
|
||||
->where('pbg_task_detail_data_lists.status', '!=', 1);
|
||||
})
|
||||
->count();
|
||||
break;
|
||||
|
||||
case 'business-dlh':
|
||||
$bigdataResumeCount = PbgTask::where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
$q2->whereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%fungsi usaha%'])
|
||||
->orWhereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
->whereIn("status", PbgTaskStatus::getNonVerified());
|
||||
})
|
||||
->where('is_valid', true)
|
||||
->whereYear('task_created_at', $year)
|
||||
->whereExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('pbg_task_detail_data_lists')
|
||||
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
|
||||
->where('pbg_task_detail_data_lists.data_type', 5)
|
||||
->where('pbg_task_detail_data_lists.status', '!=', 1);
|
||||
})
|
||||
->count();
|
||||
break;
|
||||
|
||||
default:
|
||||
Log::info('Unknown filter for consistency validation', [
|
||||
'filter' => $filter,
|
||||
'year' => $year
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($bigdataResumeCount !== null) {
|
||||
$isConsistent = ($actualCount === $bigdataResumeCount);
|
||||
|
||||
Log::info('RequestAssignment vs BigdataResume consistency check', [
|
||||
'filter' => $filter,
|
||||
'year' => $year,
|
||||
'request_assignment_count' => $actualCount,
|
||||
'bigdata_resume_count' => $bigdataResumeCount,
|
||||
'is_consistent' => $isConsistent,
|
||||
'difference' => $actualCount - $bigdataResumeCount
|
||||
]);
|
||||
|
||||
if (!$isConsistent) {
|
||||
Log::warning('INCONSISTENCY DETECTED between RequestAssignment and BigdataResume', [
|
||||
'filter' => $filter,
|
||||
'year' => $year,
|
||||
'request_assignment_count' => $actualCount,
|
||||
'bigdata_resume_count' => $bigdataResumeCount,
|
||||
'difference' => $actualCount - $bigdataResumeCount
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error in consistency validation', [
|
||||
'error' => $e->getMessage(),
|
||||
'filter' => $filter,
|
||||
'year' => $year
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ class SpatialPlanningController extends Controller
|
||||
$search = $request->input('search', '');
|
||||
|
||||
$query = SpatialPlanning::query();
|
||||
|
||||
// Only include spatial plannings that are not yet issued (is_terbit = false)
|
||||
$query->where('is_terbit', false);
|
||||
|
||||
if ($search) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%$search%")
|
||||
@@ -42,9 +46,11 @@ class SpatialPlanningController extends Controller
|
||||
// Menambhakan nomor urut (No)
|
||||
$start = ($spatialPlannings->currentPage()-1) * $perPage + 1;
|
||||
|
||||
// Tambahkan nomor urut ke dalam data
|
||||
// Tambahkan nomor urut ke dalam data (calculated_retribution sudah auto-append)
|
||||
$data = $spatialPlannings->map(function ($item, $index) use ($start) {
|
||||
return array_merge($item->toArray(), ['no' => $start + $index]);
|
||||
$itemArray = $item->toArray();
|
||||
$itemArray['no'] = $start + $index;
|
||||
return $itemArray;
|
||||
});
|
||||
|
||||
info($data);
|
||||
@@ -104,9 +110,10 @@ class SpatialPlanningController extends Controller
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(SpatialPlanning $spatialPlanning): SpatialPlanning
|
||||
public function show(SpatialPlanning $spatialPlanning): array
|
||||
{
|
||||
return $spatialPlanning;
|
||||
// calculated_retribution and formatted_retribution are already appended via $appends
|
||||
return $spatialPlanning->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
98
app/Http/Controllers/Api/TaxationsController.php
Normal file
98
app/Http/Controllers/Api/TaxationsController.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Exports\TaxationsExport;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ExcelUploadRequest;
|
||||
use App\Http\Requests\TaxationsRequest;
|
||||
use App\Http\Resources\TaxationsResource;
|
||||
use App\Imports\TaxationsImport;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Tax;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
|
||||
class TaxationsController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
try{
|
||||
$query = Tax::query()->orderBy('id', 'desc');
|
||||
|
||||
if($request->has('search') && !empty($request->get('search'))){
|
||||
$query->where('tax_no', 'like', '%'. $request->get('search') . '%')
|
||||
->orWhere('wp_name', 'like', '%'. $request->get('search') . '%')
|
||||
->orWhere('business_name', 'like', '%'. $request->get('search') . '%');
|
||||
}
|
||||
|
||||
return TaxationsResource::collection($query->paginate(config('app.paginate_per_page', 50)));
|
||||
}catch(\Exception $e){
|
||||
Log::info($e->getMessage());
|
||||
return response()->json([
|
||||
'error' => 'Failed to get data',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function upload(ExcelUploadRequest $request)
|
||||
{
|
||||
try{
|
||||
if(!$request->hasFile('file')){
|
||||
return response()->json([
|
||||
'error' => 'No file provided'
|
||||
], 400);
|
||||
}
|
||||
|
||||
$file = $request->file('file');
|
||||
Excel::import(new TaxationsImport, $file);
|
||||
return response()->json(['message' => 'File uploaded successfully'], 200);
|
||||
}catch(\Exception $e){
|
||||
Log::info($e->getMessage());
|
||||
return response()->json([
|
||||
'error' => 'Failed to upload file',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function export(Request $request)
|
||||
{
|
||||
return Excel::download(new TaxationsExport, 'pajak_per_kecamatan.xlsx');
|
||||
}
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
try{
|
||||
$tax = Tax::find($request->id);
|
||||
$tax->delete();
|
||||
return response()->json(['message' => 'Data deleted successfully'], 200);
|
||||
}catch(\Exception $e){
|
||||
Log::info($e->getMessage());
|
||||
return response()->json([
|
||||
'error' => 'Failed to delete data',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(TaxationsRequest $request, string $id)
|
||||
{
|
||||
try{
|
||||
$tax = Tax::find($id);
|
||||
if($tax){
|
||||
$tax->update($request->validated());
|
||||
return response()->json(['message' => 'Successfully updated', new TaxationsResource($tax)]);
|
||||
} else {
|
||||
return response()->json(['message' => 'Tax not found'], 404);
|
||||
}
|
||||
}catch(\Exception $e){
|
||||
Log::info($e->getMessage());
|
||||
return response()->json([
|
||||
'error' => 'Failed to update tax',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,9 @@ class UsersController extends Controller
|
||||
return UserResource::collection($query->paginate(config('app.paginate_per_page', 50)));
|
||||
}
|
||||
public function logout(Request $request){
|
||||
$request->user()->tokens()->delete();
|
||||
\Laravel\Sanctum\PersonalAccessToken::where('tokenable_id', $request->user()->id)
|
||||
->where('tokenable_type', get_class($request->user()))
|
||||
->delete();
|
||||
return response()->json(['message' => 'logged out successfully']);
|
||||
}
|
||||
public function store(UsersRequest $request){
|
||||
|
||||
76
app/Http/Controllers/Auth/AuthenticatedSessionController.php
Executable file → Normal file
76
app/Http/Controllers/Auth/AuthenticatedSessionController.php
Executable file → Normal file
@@ -36,11 +36,77 @@ class AuthenticatedSessionController extends Controller
|
||||
// Ambil user yang sedang login
|
||||
$user = Auth::user();
|
||||
|
||||
// Buat token untuk API
|
||||
$token = $user->createToken(env('APP_KEY'))->plainTextToken;
|
||||
// Hapus token lama jika ada
|
||||
\Laravel\Sanctum\PersonalAccessToken::where('tokenable_id', $user->id)
|
||||
->where('tokenable_type', get_class($user))
|
||||
->delete();
|
||||
|
||||
// Buat token untuk API dengan scope dan expiration
|
||||
$tokenName = config('app.name', 'Laravel') . '-' . $user->id . '-' . time();
|
||||
|
||||
// Token dengan scope (opsional)
|
||||
$token = $user->createToken($tokenName, ['*'], now()->addDays(30))->plainTextToken;
|
||||
|
||||
// Simpan token di session untuk digunakan di frontend
|
||||
session(['api_token' => $token]);
|
||||
|
||||
return redirect()->intended(RouteServiceProvider::HOME);
|
||||
// Simpan timestamp login untuk validasi multi-user
|
||||
session(['login_timestamp' => now()->timestamp]);
|
||||
session(['user_id' => $user->id]);
|
||||
|
||||
// Append menu_id dynamically to HOME
|
||||
$menuId = optional(\App\Models\Menu::where('name', 'Dashboard Pimpinan SIMBG')->first())->id;
|
||||
$home = RouteServiceProvider::HOME . ($menuId ? ('?menu_id=' . $menuId) : '');
|
||||
return redirect()->intended($home);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate API token for authenticated user
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function generateApiToken(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if (!$user) {
|
||||
return response()->json(['error' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
// Delete existing tokens
|
||||
\Laravel\Sanctum\PersonalAccessToken::where('tokenable_id', $user->id)
|
||||
->where('tokenable_type', get_class($user))
|
||||
->delete();
|
||||
|
||||
// Generate new token
|
||||
$tokenName = config('app.name', 'Laravel') . '-' . $user->id . '-' . time();
|
||||
$token = $user->createToken($tokenName, ['*'], now()->addDays(30))->plainTextToken;
|
||||
|
||||
return response()->json([
|
||||
'token' => $token,
|
||||
'token_type' => 'Bearer',
|
||||
'expires_in' => 30 * 24 * 60 * 60, // 30 days in seconds
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke API token for authenticated user
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function revokeApiToken(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if (!$user) {
|
||||
return response()->json(['error' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$user->tokens()->delete();
|
||||
|
||||
return response()->json(['message' => 'All tokens revoked successfully']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,7 +118,9 @@ class AuthenticatedSessionController extends Controller
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
if($request->user()){
|
||||
$request->user()->tokens()->delete();
|
||||
\Laravel\Sanctum\PersonalAccessToken::where('tokenable_id', $request->user()->id)
|
||||
->where('tokenable_type', get_class($request->user()))
|
||||
->delete();
|
||||
}
|
||||
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
0
app/Http/Controllers/Auth/ConfirmablePasswordController.php
Executable file → Normal file
0
app/Http/Controllers/Auth/ConfirmablePasswordController.php
Executable file → Normal file
0
app/Http/Controllers/Auth/EmailVerificationNotificationController.php
Executable file → Normal file
0
app/Http/Controllers/Auth/EmailVerificationNotificationController.php
Executable file → Normal file
0
app/Http/Controllers/Auth/EmailVerificationPromptController.php
Executable file → Normal file
0
app/Http/Controllers/Auth/EmailVerificationPromptController.php
Executable file → Normal file
0
app/Http/Controllers/Auth/NewPasswordController.php
Executable file → Normal file
0
app/Http/Controllers/Auth/NewPasswordController.php
Executable file → Normal file
0
app/Http/Controllers/Auth/PasswordResetLinkController.php
Executable file → Normal file
0
app/Http/Controllers/Auth/PasswordResetLinkController.php
Executable file → Normal file
0
app/Http/Controllers/Auth/RegisteredUserController.php
Executable file → Normal file
0
app/Http/Controllers/Auth/RegisteredUserController.php
Executable file → Normal file
0
app/Http/Controllers/Auth/VerifyEmailController.php
Executable file → Normal file
0
app/Http/Controllers/Auth/VerifyEmailController.php
Executable file → Normal file
0
app/Http/Controllers/Controller.php
Executable file → Normal file
0
app/Http/Controllers/Controller.php
Executable file → Normal file
@@ -21,4 +21,13 @@ class BigDataController extends Controller
|
||||
{
|
||||
return view('dashboards.pbg');
|
||||
}
|
||||
|
||||
public function leader()
|
||||
{
|
||||
$latest_import_datasource = ImportDatasource::latest()->first();
|
||||
$latest_created = $latest_import_datasource ?
|
||||
$latest_import_datasource->created_at->format("j F Y H:i:s") : null;
|
||||
$menus = Menu::all();
|
||||
return view('dashboards.leader', compact('latest_created', 'menus'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +82,15 @@ class SpatialPlanningController extends Controller
|
||||
"kbli"=> "KBLI",
|
||||
"activities"=> "Kegiatan",
|
||||
"area"=> "Luas (m2)",
|
||||
"land_area"=> "Luas Lahan (m2)",
|
||||
"location"=> "Lokasi",
|
||||
"number"=> "Nomor",
|
||||
"date"=> "Tanggal",
|
||||
"site_bcr"=> "BCR",
|
||||
"building_function"=> "Fungsi Bangunan",
|
||||
"business_type_info"=> "Jenis Usaha",
|
||||
"is_terbit"=> "Status Terbit",
|
||||
"calculated_retribution"=> "Retribusi",
|
||||
];
|
||||
}
|
||||
|
||||
@@ -95,9 +101,15 @@ class SpatialPlanningController extends Controller
|
||||
"kbli"=> "text",
|
||||
"activities"=> "text",
|
||||
"area"=> "text",
|
||||
"land_area"=> "text",
|
||||
"location"=> "text",
|
||||
"number"=> "text",
|
||||
"date"=> "date",
|
||||
"site_bcr"=> "text",
|
||||
"building_function"=> "text",
|
||||
"business_type_info"=> "readonly",
|
||||
"is_terbit"=> "select",
|
||||
"calculated_retribution"=> "readonly",
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ use App\Http\Resources\TaskAssignmentsResource;
|
||||
use App\Models\PbgTask;
|
||||
use App\Models\TaskAssignment;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class QuickSearchController extends Controller
|
||||
{
|
||||
@@ -15,6 +17,10 @@ class QuickSearchController extends Controller
|
||||
return view("quick-search.index");
|
||||
}
|
||||
|
||||
public function public_search(){
|
||||
return view("public-search.index");
|
||||
}
|
||||
|
||||
public function search_result(Request $request){
|
||||
$keyword = $request->get("keyword");
|
||||
|
||||
@@ -24,21 +30,105 @@ class QuickSearchController extends Controller
|
||||
public function quick_search_datatable(Request $request)
|
||||
{
|
||||
try {
|
||||
$query = PbgTask::orderBy('id', 'desc');
|
||||
// Gunakan subquery untuk performa yang lebih baik dan menghindari duplikasi
|
||||
$query = PbgTask::select([
|
||||
'pbg_task.*',
|
||||
DB::raw('(SELECT name_building FROM pbg_task_details WHERE pbg_task_details.pbg_task_uid = pbg_task.uuid LIMIT 1) as name_building'),
|
||||
DB::raw('(SELECT nilai_retribusi_bangunan FROM pbg_task_retributions WHERE pbg_task_retributions.pbg_task_uid = pbg_task.uuid LIMIT 1) as nilai_retribusi_bangunan'),
|
||||
DB::raw('(SELECT note FROM pbg_statuses WHERE pbg_statuses.pbg_task_uuid = pbg_task.uuid LIMIT 1) as note')
|
||||
])
|
||||
->orderBy('pbg_task.id', 'desc');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->get('search');
|
||||
$search = trim($request->get('search'));
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'LIKE', "%$search%")
|
||||
->orWhere('registration_number', 'LIKE', "%$search%")
|
||||
->orWhere('address', 'LIKE', "%$search%")
|
||||
->orWhere('document_number', 'LIKE', "%$search%");
|
||||
$q->where('pbg_task.registration_number', 'LIKE', "%$search%")
|
||||
->orWhere('pbg_task.name', 'LIKE', "%$search%")
|
||||
->orWhere('pbg_task.owner_name', 'LIKE', "%$search%")
|
||||
->orWhere('pbg_task.address', 'LIKE', "%$search%")
|
||||
->orWhereExists(function ($subQuery) use ($search) {
|
||||
$subQuery->select(DB::raw(1))
|
||||
->from('pbg_task_details')
|
||||
->whereColumn('pbg_task_details.pbg_task_uid', 'pbg_task.uuid')
|
||||
->where('pbg_task_details.name_building', 'LIKE', "%$search%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return response()->json($query->paginate());
|
||||
} catch (\Throwable $e) {
|
||||
\Log::error("Error fetching datatable data: " . $e->getMessage());
|
||||
Log::error("Error fetching datatable data: " . $e->getMessage());
|
||||
return response()->json([
|
||||
'message' => 'Terjadi kesalahan saat mengambil data.',
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function public_search_datatable(Request $request)
|
||||
{
|
||||
try {
|
||||
// Hanya proses jika ada keyword search
|
||||
if (!$request->filled('search') || trim($request->get('search')) === '') {
|
||||
return response()->json([
|
||||
'data' => [],
|
||||
'total' => 0,
|
||||
'current_page' => 1,
|
||||
'last_page' => 1,
|
||||
'per_page' => 15,
|
||||
'from' => null,
|
||||
'to' => null
|
||||
]);
|
||||
}
|
||||
|
||||
$search = trim($request->get('search'));
|
||||
|
||||
// Validasi minimal 3 karakter
|
||||
if (strlen($search) < 3) {
|
||||
return response()->json([
|
||||
'data' => [],
|
||||
'total' => 0,
|
||||
'current_page' => 1,
|
||||
'last_page' => 1,
|
||||
'per_page' => 15,
|
||||
'from' => null,
|
||||
'to' => null,
|
||||
'message' => 'Minimal 3 karakter untuk pencarian'
|
||||
]);
|
||||
}
|
||||
|
||||
// Gunakan subquery untuk performa yang lebih baik dan menghindari duplikasi
|
||||
$query = PbgTask::select([
|
||||
'pbg_task.*',
|
||||
DB::raw('(SELECT name_building FROM pbg_task_details WHERE pbg_task_details.pbg_task_uid = pbg_task.uuid LIMIT 1) as name_building'),
|
||||
DB::raw('(SELECT nilai_retribusi_bangunan FROM pbg_task_retributions WHERE pbg_task_retributions.pbg_task_uid = pbg_task.uuid LIMIT 1) as nilai_retribusi_bangunan'),
|
||||
DB::raw('(SELECT note FROM pbg_statuses WHERE pbg_statuses.pbg_task_uuid = pbg_task.uuid LIMIT 1) as note')
|
||||
])
|
||||
->where(function ($q) use ($search) {
|
||||
$q->where('pbg_task.registration_number', 'LIKE', "%$search%")
|
||||
->orWhere('pbg_task.name', 'LIKE', "%$search%")
|
||||
->orWhere('pbg_task.owner_name', 'LIKE', "%$search%")
|
||||
->orWhere('pbg_task.address', 'LIKE', "%$search%")
|
||||
->orWhereExists(function ($subQuery) use ($search) {
|
||||
$subQuery->select(DB::raw(1))
|
||||
->from('pbg_task_details')
|
||||
->whereColumn('pbg_task_details.pbg_task_uid', 'pbg_task.uuid')
|
||||
->where('pbg_task_details.name_building', 'LIKE', "%$search%");
|
||||
});
|
||||
})
|
||||
->orderBy('pbg_task.id', 'desc');
|
||||
|
||||
$result = $query->paginate();
|
||||
|
||||
// Tambahkan message jika tidak ada hasil
|
||||
if ($result->total() === 0) {
|
||||
$result = $result->toArray();
|
||||
$result['message'] = 'Tidak ada data yang ditemukan';
|
||||
}
|
||||
|
||||
return response()->json($result);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error("Error fetching datatable data: " . $e->getMessage());
|
||||
return response()->json([
|
||||
'message' => 'Terjadi kesalahan saat mengambil data.',
|
||||
'error' => $e->getMessage(),
|
||||
@@ -52,7 +142,8 @@ class QuickSearchController extends Controller
|
||||
$data = PbgTask::with([
|
||||
'pbg_task_retributions',
|
||||
'pbg_task_index_integrations',
|
||||
'pbg_task_retributions.pbg_task_prasarana'
|
||||
'pbg_task_retributions.pbg_task_prasarana',
|
||||
'pbg_status'
|
||||
])->findOrFail($id);
|
||||
|
||||
$statusOptions = PbgTaskStatus::getStatuses();
|
||||
@@ -60,10 +151,10 @@ class QuickSearchController extends Controller
|
||||
|
||||
return view("quick-search.detail", compact("data", 'statusOptions', 'applicationTypes'));
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||
\Log::warning("PbgTask with ID {$id} not found.");
|
||||
Log::warning("PbgTask with ID {$id} not found.");
|
||||
return redirect()->route('quick-search.index')->with('error', 'Data tidak ditemukan.');
|
||||
} catch (\Throwable $e) {
|
||||
\Log::error("Error in QuickSearchController@show: " . $e->getMessage());
|
||||
Log::error("Error in QuickSearchController@show: " . $e->getMessage());
|
||||
return response()->view('pages.404', [], 500); // Optional: create `resources/views/errors/500.blade.php`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,10 +56,24 @@ class PbgTaskController extends Controller
|
||||
*/
|
||||
public function show(string $id)
|
||||
{
|
||||
$data = PbgTask::with(['pbg_task_retributions','pbg_task_index_integrations', 'pbg_task_retributions.pbg_task_prasarana'])->findOrFail($id);
|
||||
$data = PbgTask::with([
|
||||
'pbg_task_retributions',
|
||||
'pbg_task_index_integrations',
|
||||
'pbg_task_retributions.pbg_task_prasarana',
|
||||
'pbg_task_detail',
|
||||
'pbg_status',
|
||||
'dataLists' => function($query) {
|
||||
$query->orderBy('data_type')->orderBy('name');
|
||||
}
|
||||
])->findOrFail($id);
|
||||
|
||||
// Group data lists by data_type for easier display
|
||||
$dataListsByType = $data->dataLists->groupBy('data_type');
|
||||
|
||||
$statusOptions = PbgTaskStatus::getStatuses();
|
||||
$applicationTypes = PbgTaskApplicationTypes::labels();
|
||||
return view("pbg_task.show", compact("data", 'statusOptions', 'applicationTypes'));
|
||||
|
||||
return view("pbg_task.show", compact("data", 'statusOptions', 'applicationTypes', 'dataListsByType'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
0
app/Http/Controllers/RoutingController.php
Executable file → Normal file
0
app/Http/Controllers/RoutingController.php
Executable file → Normal file
78
app/Http/Controllers/TaxationController.php
Normal file
78
app/Http/Controllers/TaxationController.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Tax;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TaxationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$menuId = $request->query('menu_id') ?? $request->input('menu_id');
|
||||
$permissions = $this->permissions[$menuId]?? []; // Avoid undefined index error
|
||||
$creator = $permissions['allow_create'] ?? 0;
|
||||
$updater = $permissions['allow_update'] ?? 0;
|
||||
$destroyer = $permissions['allow_destroy'] ?? 0;
|
||||
return view('taxation.index', compact('creator', 'updater', 'destroyer', 'menuId'));
|
||||
}
|
||||
|
||||
public function upload(Request $request)
|
||||
{
|
||||
$menuId = $request->query('menu_id') ?? $request->input('menu_id');
|
||||
return view('taxation.upload', compact('menuId'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(string $id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(Request $request, string $id)
|
||||
{
|
||||
$menuId = $request->query('menu_id') ?? $request->input('menu_id');
|
||||
$data = Tax::find($id);
|
||||
return view('taxation.edit', compact('menuId', 'data'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, string $id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(string $id)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
167
app/Http/Middleware/ValidateApiTokenForWeb.php
Normal file
167
app/Http/Middleware/ValidateApiTokenForWeb.php
Normal file
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Laravel\Sanctum\PersonalAccessToken;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ValidateApiTokenForWeb
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
* Middleware ini memvalidasi token API untuk web requests
|
||||
* dan melakukan auto-logout jika token tidak valid
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// Skip validation untuk non-authenticated routes
|
||||
if (!Auth::check()) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Skip validation untuk API routes (sudah ditangani oleh auth:sanctum)
|
||||
if ($request->is('api/*')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
$sessionToken = Session::get('api_token');
|
||||
|
||||
// Jika tidak ada token di session, generate token baru
|
||||
if (!$sessionToken) {
|
||||
$this->generateNewToken($user);
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Validasi token API
|
||||
if (!$this->isTokenValid($sessionToken, $user)) {
|
||||
// Token invalid, check apakah ada user lain yang login
|
||||
if ($this->hasOtherUserLoggedIn($user)) {
|
||||
// User lain sudah login, force logout user ini
|
||||
$this->forceLogout($request, 'User lain telah login. Silakan login ulang.');
|
||||
return $this->redirectToLogin($request, 'User lain telah login. Silakan login ulang.');
|
||||
} else {
|
||||
// Generate token baru jika tidak ada user lain
|
||||
$this->generateNewToken($user);
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check apakah token API masih valid
|
||||
*/
|
||||
private function isTokenValid($sessionToken, $user): bool
|
||||
{
|
||||
if (!$sessionToken || !$user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract plain token dari session token
|
||||
$tokenParts = explode('|', $sessionToken);
|
||||
if (count($tokenParts) !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$plainToken = $tokenParts[1];
|
||||
|
||||
// Check token di database
|
||||
$validToken = PersonalAccessToken::where('tokenable_id', $user->id)
|
||||
->where('tokenable_type', get_class($user))
|
||||
->where('token', hash('sha256', $plainToken))
|
||||
->where(function($query) {
|
||||
$query->whereNull('expires_at')
|
||||
->orWhere('expires_at', '>', now());
|
||||
})
|
||||
->first();
|
||||
|
||||
return $validToken !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check apakah ada user lain yang login (token baru dibuat)
|
||||
*/
|
||||
private function hasOtherUserLoggedIn($currentUser): bool
|
||||
{
|
||||
$sessionUserId = Session::get('user_id');
|
||||
|
||||
// Jika ada user_id di session tapi tidak match dengan current user
|
||||
if ($sessionUserId && $sessionUserId != $currentUser->id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check apakah ada token aktif lain untuk user ini
|
||||
$activeTokens = PersonalAccessToken::where('tokenable_id', $currentUser->id)
|
||||
->where('tokenable_type', get_class($currentUser))
|
||||
->where(function($query) {
|
||||
$query->whereNull('expires_at')
|
||||
->orWhere('expires_at', '>', now());
|
||||
})
|
||||
->count();
|
||||
|
||||
// Jika tidak ada token aktif, kemungkinan user lain sudah login
|
||||
return $activeTokens === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate token baru untuk user
|
||||
*/
|
||||
private function generateNewToken($user): void
|
||||
{
|
||||
// Hapus token lama
|
||||
PersonalAccessToken::where('tokenable_id', $user->id)
|
||||
->where('tokenable_type', get_class($user))
|
||||
->delete();
|
||||
|
||||
// Generate token baru
|
||||
$tokenName = config('app.name', 'Laravel') . '-' . $user->id . '-' . time();
|
||||
$token = $user->createToken($tokenName, ['*'], now()->addDays(30))->plainTextToken;
|
||||
|
||||
// Simpan token di session
|
||||
Session::put('api_token', $token);
|
||||
Session::put('user_id', $user->id);
|
||||
Session::put('login_timestamp', now()->timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force logout user dan clear semua sessions
|
||||
*/
|
||||
private function forceLogout(Request $request, string $reason = 'Session tidak valid'): void
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if ($user) {
|
||||
// Delete all tokens for this user
|
||||
PersonalAccessToken::where('tokenable_id', $user->id)
|
||||
->where('tokenable_type', get_class($user))
|
||||
->delete();
|
||||
}
|
||||
|
||||
// Clear session
|
||||
Session::forget(['api_token', 'user_id', 'login_timestamp']);
|
||||
Auth::guard('web')->logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect ke login dengan pesan error
|
||||
*/
|
||||
private function redirectToLogin(Request $request, string $message): Response
|
||||
{
|
||||
if ($request->expectsJson() || $request->ajax()) {
|
||||
return response()->json([
|
||||
'error' => $message,
|
||||
'redirect' => route('login'),
|
||||
'force_logout' => true
|
||||
], 401);
|
||||
}
|
||||
|
||||
return redirect()->route('login')->with('error', $message);
|
||||
}
|
||||
}
|
||||
0
app/Http/Requests/Auth/LoginRequest.php
Executable file → Normal file
0
app/Http/Requests/Auth/LoginRequest.php
Executable file → Normal file
@@ -22,13 +22,14 @@ class SpatialPlanningRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'string',
|
||||
'kbli' => 'string',
|
||||
'activities' => 'string',
|
||||
'area' => 'string',
|
||||
'location' => 'string',
|
||||
'number' => 'string',
|
||||
'date' => 'date_format:Y-m-d',
|
||||
'name' => 'nullable|string',
|
||||
'kbli' => 'nullable|string',
|
||||
'activities' => 'nullable|string',
|
||||
'area' => 'nullable|string',
|
||||
'location' => 'nullable|string',
|
||||
'number' => 'nullable|string',
|
||||
'date' => 'nullable|date_format:Y-m-d',
|
||||
'is_terbit' => 'nullable|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class SpatialPlanningsRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required','string','max:255'],
|
||||
'kbli' => ['required','string','max:255'],
|
||||
'kegiatan' => ['required','string'],
|
||||
'luas' => ['required','numeric','regex:/^\d{1,16}(\.\d{1,2})?$/'],
|
||||
'lokasi' => ['required','string'],
|
||||
'nomor' => ['required','string','max:255',Rule::unique('spatial_plannings')->ignore($this->id)],
|
||||
'sp_date' => ['required','date'],
|
||||
];
|
||||
}
|
||||
}
|
||||
38
app/Http/Requests/TaxationsRequest.php
Normal file
38
app/Http/Requests/TaxationsRequest.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class TaxationsRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'tax_no' => ['required', 'string', Rule::unique('taxs')->ignore($this->id)],
|
||||
'tax_code' => ['required', 'string'],
|
||||
'wp_name' => ['required', 'string'],
|
||||
'business_name' => ['required', 'string'],
|
||||
'address' => ['required', 'string'],
|
||||
'start_validity' => ['required', 'date_format:Y-m-d'],
|
||||
'end_validity' => ['required', 'date_format:Y-m-d'],
|
||||
'tax_value' => ['required', 'numeric', 'regex:/^\d{1,16}(\.\d{1,2})?$/'],
|
||||
'subdistrict' => ['required', 'string'],
|
||||
'village' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,8 @@ class DataSettingResource extends JsonResource
|
||||
'key' => $this->key,
|
||||
'value' => $this->value,
|
||||
'type' => $this->type,
|
||||
'created_at' => $this->created_at->toDateTimeString(),
|
||||
'updated_at' => $this->updated_at->toDateTimeString(),
|
||||
'created_at' => $this->created_at ? $this->created_at->toDateTimeString() : null,
|
||||
'updated_at' => $this->updated_at ? $this->updated_at->toDateTimeString() : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@ class RequestAssignmentResouce extends JsonResource
|
||||
->where('pbg_type', 'bukti_bayar')
|
||||
->sortByDesc('created_at')
|
||||
->first(),
|
||||
'pbg_task_retributions' => $this->pbg_task_retributions,
|
||||
'pbg_task_detail' => $this->pbg_task_detail,
|
||||
'pbg_status' => $this->pbg_status,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace App\Http\Resources;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class SpatialPlanningsResource extends JsonResource
|
||||
class TaxationsResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
78
app/Imports/TaxationsImport.php
Normal file
78
app/Imports/TaxationsImport.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Imports;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Maatwebsite\Excel\Concerns\ToCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithBatchInserts;
|
||||
use Maatwebsite\Excel\Concerns\WithChunkReading;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
||||
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use App\Models\Tax;
|
||||
|
||||
class TaxationsImport implements ToCollection, WithMultipleSheets, WithChunkReading, WithBatchInserts, ShouldQueue, WithHeadingRow
|
||||
{
|
||||
/**
|
||||
* @param Collection $collection
|
||||
*/
|
||||
public function collection(Collection $collection)
|
||||
{
|
||||
$batchData = [];
|
||||
$batchSize = 1000;
|
||||
|
||||
foreach ($collection as $row) {
|
||||
|
||||
$masaPajak = trim($row['masa_pajak']) ?? '';
|
||||
|
||||
$masaParts = explode('-', $masaPajak);
|
||||
|
||||
$startValidity = null;
|
||||
$endValidity = null;
|
||||
|
||||
if (count($masaParts) === 2) {
|
||||
$startValidity = \Carbon\Carbon::createFromFormat('d/m/Y', trim($masaParts[0]))->format('Y-m-d');
|
||||
$endValidity = \Carbon\Carbon::createFromFormat('d/m/Y', trim($masaParts[1]))->format('Y-m-d');
|
||||
}
|
||||
|
||||
$batchData[] = [
|
||||
'tax_code' => trim($row['kode']) ?? '',
|
||||
'tax_no' => trim($row['no']) ?? '',
|
||||
'npwpd' => trim($row['npwpd']) ?? '',
|
||||
'wp_name' => trim($row['nama_wp']) ?? '',
|
||||
'business_name' => trim($row['nama_usaha']) ?? '',
|
||||
'address' => trim($row['alamat_usaha']) ?? '',
|
||||
'start_validity' => $startValidity,
|
||||
'end_validity' => $endValidity,
|
||||
'tax_value' => (float) str_replace(',', '', trim($row['nilai_pajak']) ?? '0'),
|
||||
'subdistrict' => trim($row['kecamatan']) ?? '',
|
||||
'village' => trim($row['desa']) ?? '',
|
||||
];
|
||||
|
||||
if (count($batchData) >= $batchSize) {
|
||||
Tax::upsert($batchData, ['tax_no'], ['tax_code', 'tax_no', 'npwpd', 'wp_name', 'business_name', 'address', 'start_validity', 'end_validity', 'tax_value', 'subdistrict', 'village']);
|
||||
$batchData = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($batchData)) {
|
||||
Tax::upsert($batchData, ['tax_no'], ['tax_code', 'tax_no', 'npwpd', 'wp_name', 'business_name', 'address', 'start_validity', 'end_validity', 'tax_value', 'subdistrict', 'village']);
|
||||
}
|
||||
|
||||
}
|
||||
public function sheets(): array {
|
||||
return [
|
||||
0 => $this
|
||||
];
|
||||
}
|
||||
|
||||
public function chunkSize(): int
|
||||
{
|
||||
return 1000;
|
||||
}
|
||||
|
||||
public function batchSize(): int
|
||||
{
|
||||
return 1000;
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,11 @@ use App\Models\ImportDatasource;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use App\Services\ServiceTabPbgTask;
|
||||
use App\Services\ServiceGoogleSheet;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class RetrySyncronizeJob implements ShouldQueue
|
||||
{
|
||||
use Queueable, Dispatchable, InteractsWithQueue, SerializesModels;
|
||||
@@ -28,7 +29,6 @@ class RetrySyncronizeJob implements ShouldQueue
|
||||
{
|
||||
try{
|
||||
$service_tab_pbg_task = app(ServiceTabPbgTask::class);
|
||||
$service_google_sheet = app(ServiceGoogleSheet::class);
|
||||
|
||||
$failed_import = ImportDatasource::find($this->import_datasource_id);
|
||||
|
||||
@@ -46,10 +46,7 @@ class RetrySyncronizeJob implements ShouldQueue
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$data_setting_result = $service_google_sheet->get_big_resume_data();
|
||||
|
||||
BigdataResume::generateResumeData($failed_import->id, "all", $data_setting_result);
|
||||
BigdataResume::generateResumeData($failed_import->id, now()->year, $data_setting_result);
|
||||
BigdataResume::generateResumeData($failed_import->id, date('Y'), "simbg");
|
||||
|
||||
$failed_import->update([
|
||||
'status' => ImportDatasourceStatus::Success->value,
|
||||
@@ -58,7 +55,7 @@ class RetrySyncronizeJob implements ShouldQueue
|
||||
'failed_uuid' => null
|
||||
]);
|
||||
}catch(\Exception $e){
|
||||
\Log::error("RetrySyncronizeJob Failed: ". $e->getMessage(), [
|
||||
Log::error("RetrySyncronizeJob Failed: ". $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
]);
|
||||
if(isset($failed_import)){
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Jobs;
|
||||
|
||||
use App\Models\BigdataResume;
|
||||
use App\Models\ImportDatasource;
|
||||
use App\Models\PbgTask;
|
||||
use App\Services\ServiceGoogleSheet;
|
||||
use App\Services\ServicePbgTask;
|
||||
use App\Services\ServiceTabPbgTask;
|
||||
@@ -21,72 +22,208 @@ class ScrapingDataJob implements ShouldQueue
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Inject dependencies instead of creating them inside.
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
) {
|
||||
public function __construct()
|
||||
{
|
||||
// Use dedicated scraping queue
|
||||
$this->queue = 'scraping';
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
* Execute the job with optimized schema:
|
||||
* 1. Scrape Google Sheet first
|
||||
* 2. Scrape PBG Task to get parent data
|
||||
* 3. Loop through parent tasks to scrape details via ServiceTabPbgTask
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
$import_datasource = null;
|
||||
$failed_uuid = null;
|
||||
$processedTasks = 0;
|
||||
$totalTasks = 0;
|
||||
|
||||
$client = app(Client::class);
|
||||
try {
|
||||
Log::info("=== SCRAPING DATA JOB STARTED ===");
|
||||
|
||||
// Initialize services
|
||||
$service_google_sheet = app(ServiceGoogleSheet::class);
|
||||
$service_pbg_task = app(ServicePbgTask::class);
|
||||
$service_tab_pbg_task = app(ServiceTabPbgTask::class);
|
||||
$service_google_sheet = app(ServiceGoogleSheet::class);
|
||||
$service_token = app(ServiceTokenSIMBG::class);
|
||||
// Create a record with "processing" status
|
||||
|
||||
// Create ImportDatasource record
|
||||
$import_datasource = ImportDatasource::create([
|
||||
'message' => 'Initiating scraping...',
|
||||
'message' => 'Starting optimized scraping process...',
|
||||
'response_body' => null,
|
||||
'status' => 'processing',
|
||||
'start_time' => now(),
|
||||
'failed_uuid' => null
|
||||
]);
|
||||
|
||||
$failed_uuid = null;
|
||||
Log::info("ImportDatasource created", ['id' => $import_datasource->id]);
|
||||
|
||||
// STEP 1: Scrape Google Sheet data first
|
||||
Log::info("=== STEP 1: SCRAPING GOOGLE SHEET ===");
|
||||
$import_datasource->update(['message' => 'Scraping Google Sheet data...']);
|
||||
|
||||
// Run the scraping services
|
||||
$service_google_sheet->run_service();
|
||||
Log::info("Google Sheet scraping completed successfully");
|
||||
|
||||
// STEP 2: Scrape PBG Task to get parent data
|
||||
Log::info("=== STEP 2: SCRAPING PBG TASK PARENT DATA ===");
|
||||
$import_datasource->update(['message' => 'Scraping PBG Task parent data...']);
|
||||
|
||||
$service_pbg_task->run_service();
|
||||
Log::info("PBG Task parent data scraping completed");
|
||||
|
||||
// STEP 3: Get all PBG tasks for detail scraping
|
||||
$totalTasks = PbgTask::count();
|
||||
Log::info("=== STEP 3: SCRAPING PBG TASK DETAILS ===", [
|
||||
'total_tasks' => $totalTasks
|
||||
]);
|
||||
|
||||
$import_datasource->update([
|
||||
'message' => "Scraping details for {$totalTasks} PBG tasks..."
|
||||
]);
|
||||
|
||||
// Process tasks in chunks for memory efficiency
|
||||
$chunkSize = 100;
|
||||
$processedTasks = 0;
|
||||
|
||||
PbgTask::orderBy('id')->chunk($chunkSize, function ($pbg_tasks) use (
|
||||
$service_tab_pbg_task,
|
||||
&$processedTasks,
|
||||
$totalTasks,
|
||||
$import_datasource,
|
||||
&$failed_uuid
|
||||
) {
|
||||
foreach ($pbg_tasks as $pbg_task) {
|
||||
try {
|
||||
$service_tab_pbg_task->run_service();
|
||||
}catch(\Exception $e){
|
||||
$failed_uuid = $service_tab_pbg_task->getFailedUUID();
|
||||
throw $e;
|
||||
// Scrape all details for this task
|
||||
$this->processTaskDetails($service_tab_pbg_task, $pbg_task->uuid);
|
||||
|
||||
$processedTasks++;
|
||||
|
||||
// Update progress every 10 tasks
|
||||
if ($processedTasks % 10 === 0) {
|
||||
$progress = round(($processedTasks / $totalTasks) * 100, 2);
|
||||
Log::info("Progress update", [
|
||||
'processed' => $processedTasks,
|
||||
'total' => $totalTasks,
|
||||
'progress' => "{$progress}%"
|
||||
]);
|
||||
|
||||
$import_datasource->update([
|
||||
'message' => "Processing details: {$processedTasks}/{$totalTasks} ({$progress}%)"
|
||||
]);
|
||||
}
|
||||
|
||||
$data_setting_result = $service_google_sheet->get_big_resume_data();
|
||||
} catch (\Exception $e) {
|
||||
Log::warning("Failed to process task details", [
|
||||
'uuid' => $pbg_task->uuid,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
BigdataResume::generateResumeData($import_datasource->id, "all", $data_setting_result);
|
||||
BigdataResume::generateResumeData($import_datasource->id, now()->year, $data_setting_result);
|
||||
// Store failed UUID but continue processing
|
||||
$failed_uuid = $pbg_task->uuid;
|
||||
|
||||
// Update status to success
|
||||
// Only stop if it's a critical error
|
||||
if ($this->isCriticalError($e)) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Log::info("Task details scraping completed", [
|
||||
'processed_tasks' => $processedTasks,
|
||||
'total_tasks' => $totalTasks
|
||||
]);
|
||||
|
||||
// STEP 4: Generate BigData Resume
|
||||
Log::info("=== STEP 4: GENERATING BIGDATA RESUME ===");
|
||||
$import_datasource->update(['message' => 'Generating BigData resume...']);
|
||||
|
||||
BigdataResume::generateResumeData($import_datasource->id, date('Y'), "simbg");
|
||||
|
||||
Log::info("BigData resume generated successfully");
|
||||
|
||||
// Update final status
|
||||
$import_datasource->update([
|
||||
'status' => 'success',
|
||||
'message' => 'Scraping completed successfully.',
|
||||
'finish_time' => now()
|
||||
'message' => "Scraping completed successfully. Processed {$processedTasks}/{$totalTasks} tasks.",
|
||||
'finish_time' => now(),
|
||||
'failed_uuid' => $failed_uuid // Store last failed UUID if any
|
||||
]);
|
||||
|
||||
Log::info("=== SCRAPING DATA JOB COMPLETED SUCCESSFULLY ===", [
|
||||
'import_datasource_id' => $import_datasource->id,
|
||||
'processed_tasks' => $processedTasks,
|
||||
'total_tasks' => $totalTasks,
|
||||
'has_failures' => !is_null($failed_uuid)
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Scraping failed: ' . $e->getMessage(), ['trace' => $e->getTraceAsString()]);
|
||||
Log::error('=== SCRAPING DATA JOB FAILED ===', [
|
||||
'error' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'processed_tasks' => $processedTasks,
|
||||
'total_tasks' => $totalTasks,
|
||||
'failed_uuid' => $failed_uuid,
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
// Update status to failed
|
||||
if (isset($import_datasource)) {
|
||||
// Update ImportDatasource with failure info
|
||||
if ($import_datasource) {
|
||||
$import_datasource->update([
|
||||
'status' => 'failed',
|
||||
'response_body' => 'Terjadi kesalahan, Syncronize tidak selesai',
|
||||
'message' => "Scraping failed: {$e->getMessage()}. Processed {$processedTasks}/{$totalTasks} tasks.",
|
||||
'response_body' => 'Scraping process interrupted due to error',
|
||||
'finish_time' => now(),
|
||||
'failed_uuid' => $failed_uuid,
|
||||
]);
|
||||
}
|
||||
|
||||
// Mark the job as failed
|
||||
// Don't retry this job
|
||||
$this->fail($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process all detail endpoints for a single PBG task
|
||||
*/
|
||||
private function processTaskDetails(ServiceTabPbgTask $service, string $uuid): void
|
||||
{
|
||||
// Call all detail scraping methods for this task
|
||||
$service->scraping_task_details($uuid);
|
||||
$service->scraping_pbg_data_list($uuid);
|
||||
$service->scraping_task_retributions($uuid);
|
||||
$service->scraping_task_integrations($uuid);
|
||||
$service->scraping_task_detail_status($uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an error is critical enough to stop the entire process
|
||||
*/
|
||||
private function isCriticalError(\Exception $e): bool
|
||||
{
|
||||
$criticalMessages = [
|
||||
'authentication failed',
|
||||
'token expired',
|
||||
'database connection',
|
||||
'memory exhausted',
|
||||
'maximum execution time'
|
||||
];
|
||||
|
||||
$errorMessage = strtolower($e->getMessage());
|
||||
|
||||
foreach ($criticalMessages as $critical) {
|
||||
if (strpos($errorMessage, $critical) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\ImportDatasource;
|
||||
use App\Services\GoogleSheetService;
|
||||
use App\Services\ServiceGoogleSheet;
|
||||
use App\Services\ServicePbgTask;
|
||||
use App\Services\ServiceTabPbgTask;
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class SyncronizeSIMBG implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $tries = 1;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Avoid injecting non-serializable dependencies here
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$import_datasource = ImportDatasource::where('status', 'processing')->first();
|
||||
|
||||
if (!$import_datasource) {
|
||||
$import_datasource = ImportDatasource::create([
|
||||
'message' => 'Initiating scraping...',
|
||||
'response_body' => null,
|
||||
'status' => 'processing'
|
||||
]);
|
||||
}
|
||||
try {
|
||||
// Create an instance of GuzzleHttp\Client inside handle()
|
||||
$client = new Client();
|
||||
|
||||
// Create instances of services inside handle()
|
||||
$service_pbg_task = app(ServicePbgTask::class);
|
||||
$service_tab_pbg_task = app(ServiceTabPbgTask::class);
|
||||
|
||||
// Create a record with "processing" status
|
||||
|
||||
|
||||
// Run the service
|
||||
$service_google_sheet = new ServiceGoogleSheet();
|
||||
\Log::info('Starting Google Sheet service');
|
||||
$service_google_sheet->run_service();
|
||||
\Log::info('Google Sheet service completed');
|
||||
|
||||
\Log::info('Starting PBG Task service');
|
||||
$service_pbg_task->run_service();
|
||||
\Log::info('PBG Task service completed');
|
||||
|
||||
\Log::info('Starting Tab PBG Task service');
|
||||
$service_tab_pbg_task->run_service();
|
||||
\Log::info('Tab PBG Task service completed');
|
||||
|
||||
// Update the record status to "success" after completion
|
||||
$import_datasource->update([
|
||||
'status' => 'success',
|
||||
'message' => 'Scraping completed successfully.'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error("SyncronizeSIMBG Job Failed: " . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
]);
|
||||
$import_datasource->update([
|
||||
'status' => 'failed',
|
||||
'message' => 'Failed job'
|
||||
]);
|
||||
$this->fail($e); // Mark the job as failed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Enums\PbgTaskStatus;
|
||||
use App\Services\ServiceGoogleSheet;
|
||||
|
||||
class BigdataResume extends Model
|
||||
{
|
||||
@@ -30,6 +32,12 @@ class BigdataResume extends Model
|
||||
'issuance_realization_pbg_sum',
|
||||
'process_in_technical_office_count',
|
||||
'process_in_technical_office_sum',
|
||||
'business_rab_count',
|
||||
'business_krk_count',
|
||||
'business_dlh_count',
|
||||
'non_business_rab_count',
|
||||
'non_business_krk_count',
|
||||
'resume_type',
|
||||
];
|
||||
|
||||
public function importDatasource()
|
||||
@@ -37,101 +45,336 @@ class BigdataResume extends Model
|
||||
return $this->belongsTo(ImportDatasource::class, 'import_datasource_id');
|
||||
}
|
||||
|
||||
public static function generateResumeData($import_datasource_id, $year, $data_setting){
|
||||
$stats = PbgTask::with(['googleSheet', 'pbg_task_retributions'])
|
||||
->leftJoin('pbg_task_retributions as ptr', 'pbg_task.uuid', '=', 'ptr.pbg_task_uid')
|
||||
->leftJoin('pbg_task_google_sheet as ptgs', 'pbg_task.registration_number', '=', 'ptgs.no_registrasi')
|
||||
->when($year !== 'all', function ($query) use ($year) {
|
||||
$query->whereYear('pbg_task.task_created_at', (int) $year);
|
||||
public static function generateResumeData($import_datasource_id, $year, $resume_type){
|
||||
// Get accurate counts without joins to avoid duplicates from multiple retributions
|
||||
// Filter only valid data (is_valid = true)
|
||||
$verified_count = PbgTask::whereIn('status', PbgTaskStatus::getVerified())
|
||||
->where('is_valid', true)
|
||||
->where('pbg_task.due_date', '>=', $year.'-02-01')
|
||||
->count();
|
||||
$non_verified_count = PbgTask::whereIn('status', PbgTaskStatus::getNonVerified())
|
||||
->where('is_valid', true)
|
||||
->where('pbg_task.due_date', '>=', $year.'-02-01')
|
||||
->count();
|
||||
$waiting_click_dpmptsp_count = PbgTask::whereIn('status', PbgTaskStatus::getWaitingClickDpmptsp())
|
||||
->where('is_valid', true)
|
||||
->where('pbg_task.due_date', '>=', $year.'-02-01')
|
||||
->count();
|
||||
$issuance_realization_pbg_count = PbgTask::whereIn('status', PbgTaskStatus::getIssuanceRealizationPbg())
|
||||
->where('is_valid', true)
|
||||
->where('pbg_task.due_date', '>=', $year.'-02-01')
|
||||
->count();
|
||||
$process_in_technical_office_count = PbgTask::whereIn('status', PbgTaskStatus::getProcessInTechnicalOffice())
|
||||
->where('is_valid', true)
|
||||
->where('pbg_task.due_date', '>=', $year.'-02-01')
|
||||
->count();
|
||||
$potention_count = PbgTask::whereIn('status', PbgTaskStatus::getPotention())
|
||||
->where('is_valid', true)
|
||||
->where('pbg_task.due_date', '>=', $year.'-02-01')
|
||||
->count();
|
||||
|
||||
// Business count: function_type LIKE usaha OR (non-business with unit > 1)
|
||||
$business_count = PbgTask::where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
// Traditional business: function_type LIKE usaha
|
||||
$q2->where(function ($q3) {
|
||||
$q3->whereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%fungsi usaha%'])
|
||||
->orWhereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
// OR non-business with unit > 1 (becomes business)
|
||||
->orWhere(function ($q3) {
|
||||
$q3->where(function ($q4) {
|
||||
$q4->where(function ($q5) {
|
||||
$q5->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
|
||||
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
->orWhereNull('function_type');
|
||||
})
|
||||
->whereHas('pbg_task_detail', function ($q4) {
|
||||
$q4->where('unit', '>', 1);
|
||||
});
|
||||
});
|
||||
})
|
||||
->whereIn("status", PbgTaskStatus::getNonVerified());
|
||||
})
|
||||
->where('is_valid', true)
|
||||
->where('due_date', '>=', $year.'-02-01')
|
||||
->count();
|
||||
|
||||
// Non-business count: function_type NOT LIKE usaha AND (unit IS NULL OR unit <= 1)
|
||||
$non_business_count = PbgTask::where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
$q2->where(function ($q3) {
|
||||
$q3->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
|
||||
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
->orWhereNull('function_type');
|
||||
})
|
||||
->whereIn("status", PbgTaskStatus::getNonVerified())
|
||||
// Additional condition: unit IS NULL OR unit <= 1
|
||||
->where(function ($q3) {
|
||||
$q3->whereDoesntHave('pbg_task_detail', function ($q4) {
|
||||
$q4->where('unit', '>', 1);
|
||||
})
|
||||
->orWhereDoesntHave('pbg_task_detail');
|
||||
});
|
||||
})
|
||||
->where('is_valid', true)
|
||||
->where('due_date', '>=', $year.'-02-01')
|
||||
->count();
|
||||
|
||||
// Business RAB count - for each business task with data_type=3:
|
||||
// if any status != 1 then return 1, if all status = 1 then return 0, then sum all
|
||||
$business_rab_count = DB::table('pbg_task')
|
||||
->where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
$q2->whereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%fungsi usaha%'])
|
||||
->orWhereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
->whereIn("status", PbgTaskStatus::getNonVerified());
|
||||
})
|
||||
->where('is_valid', true)
|
||||
->where('due_date', '>=', $year.'-02-01')
|
||||
->whereExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('pbg_task_detail_data_lists')
|
||||
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
|
||||
->where('pbg_task_detail_data_lists.data_type', 3);
|
||||
})
|
||||
->selectRaw('
|
||||
SUM(
|
||||
CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM pbg_task_detail_data_lists ptddl
|
||||
WHERE ptddl.pbg_task_uuid = pbg_task.uuid
|
||||
AND ptddl.data_type = 3
|
||||
AND ptddl.status != 1
|
||||
) THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
) as total_count
|
||||
')
|
||||
->value('total_count') ?? 0;
|
||||
|
||||
// Business KRK count - for each business task with data_type=2:
|
||||
// if any status != 1 then return 1, if all status = 1 then return 0, then sum all
|
||||
$business_krk_count = DB::table('pbg_task')
|
||||
->where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
$q2->whereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%fungsi usaha%'])
|
||||
->orWhereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
->whereIn("status", PbgTaskStatus::getNonVerified());
|
||||
})
|
||||
->where('is_valid', true)
|
||||
->where('due_date', '>=', $year.'-02-01')
|
||||
->whereExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('pbg_task_detail_data_lists')
|
||||
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
|
||||
->where('pbg_task_detail_data_lists.data_type', 2);
|
||||
})
|
||||
->selectRaw('
|
||||
SUM(
|
||||
CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM pbg_task_detail_data_lists ptddl
|
||||
WHERE ptddl.pbg_task_uuid = pbg_task.uuid
|
||||
AND ptddl.data_type = 2
|
||||
AND ptddl.status != 1
|
||||
) THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
) as total_count
|
||||
')
|
||||
->value('total_count') ?? 0;
|
||||
|
||||
// Business DLH count - for each business task with data_type=5:
|
||||
// if any status != 1 then return 1, if all status = 1 then return 0, then sum all
|
||||
$business_dlh_count = DB::table('pbg_task')
|
||||
->where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
$q2->whereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%fungsi usaha%'])
|
||||
->orWhereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
->whereIn("status", PbgTaskStatus::getNonVerified());
|
||||
})
|
||||
->where('is_valid', true)
|
||||
->where('due_date', '>=', $year.'-02-01')
|
||||
->whereExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('pbg_task_detail_data_lists')
|
||||
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
|
||||
->where('pbg_task_detail_data_lists.data_type', 5);
|
||||
})
|
||||
->selectRaw('
|
||||
SUM(
|
||||
CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM pbg_task_detail_data_lists ptddl
|
||||
WHERE ptddl.pbg_task_uuid = pbg_task.uuid
|
||||
AND ptddl.data_type = 5
|
||||
AND ptddl.status != 1
|
||||
) THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
) as total_count
|
||||
')
|
||||
->value('total_count') ?? 0;
|
||||
|
||||
// Non-Business RAB count - for each non-business task with data_type=3:
|
||||
// if any status != 1 then return 1, if all status = 1 then return 0, then sum all
|
||||
$non_business_rab_count = DB::table('pbg_task')
|
||||
->where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
$q2->where(function ($q3) {
|
||||
$q3->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
|
||||
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
->orWhereNull('function_type');
|
||||
})
|
||||
->whereIn("status", PbgTaskStatus::getNonVerified());
|
||||
})
|
||||
->where('is_valid', true)
|
||||
->where('due_date', '>=', $year.'-02-01')
|
||||
->whereExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('pbg_task_detail_data_lists')
|
||||
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
|
||||
->where('pbg_task_detail_data_lists.data_type', 3);
|
||||
})
|
||||
->selectRaw('
|
||||
SUM(
|
||||
CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM pbg_task_detail_data_lists ptddl
|
||||
WHERE ptddl.pbg_task_uuid = pbg_task.uuid
|
||||
AND ptddl.data_type = 3
|
||||
AND ptddl.status != 1
|
||||
) THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
) as total_count
|
||||
')
|
||||
->value('total_count') ?? 0;
|
||||
|
||||
// Non-Business KRK count - for each non-business task with data_type=2:
|
||||
// if any status != 1 then return 1, if all status = 1 then return 0, then sum all
|
||||
$non_business_krk_count = DB::table('pbg_task')
|
||||
->where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
$q2->where(function ($q3) {
|
||||
$q3->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
|
||||
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
|
||||
})
|
||||
->orWhereNull('function_type');
|
||||
})
|
||||
->whereIn("status", PbgTaskStatus::getNonVerified());
|
||||
})
|
||||
->where('is_valid', true)
|
||||
->where('due_date', '>=', $year.'-02-01')
|
||||
->whereExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('pbg_task_detail_data_lists')
|
||||
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
|
||||
->where('pbg_task_detail_data_lists.data_type', 2);
|
||||
})
|
||||
->selectRaw('
|
||||
SUM(
|
||||
CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM pbg_task_detail_data_lists ptddl
|
||||
WHERE ptddl.pbg_task_uuid = pbg_task.uuid
|
||||
AND ptddl.data_type = 2
|
||||
AND ptddl.status != 1
|
||||
) THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
) as total_count
|
||||
')
|
||||
->value('total_count') ?? 0;
|
||||
|
||||
// Debug: Check if there are non-verified tasks and their retribution data
|
||||
$debug_non_verified = PbgTask::whereIn('status', PbgTaskStatus::getNonVerified())
|
||||
->where('is_valid', true)
|
||||
->where('due_date', '>=', $year.'-02-01')
|
||||
->with('pbg_task_retributions')
|
||||
->get();
|
||||
|
||||
\Log::info('Non-verified tasks debug', [
|
||||
'year' => $year,
|
||||
'non_verified_statuses' => PbgTaskStatus::getNonVerified(),
|
||||
'tasks_count' => $debug_non_verified->count(),
|
||||
'tasks_with_retribution' => $debug_non_verified->filter(fn($task) => $task->pbg_task_retributions)->count(),
|
||||
'sample_retribution_values' => $debug_non_verified->take(3)->map(fn($task) => [
|
||||
'uuid' => $task->uuid,
|
||||
'status' => $task->status,
|
||||
'has_retribution' => !is_null($task->pbg_task_retributions),
|
||||
'retribution_value' => $task->pbg_task_retributions?->nilai_retribusi_bangunan ?? 'NULL'
|
||||
])
|
||||
]);
|
||||
|
||||
// Calculate totals using count-based formula
|
||||
// Business: $business_count * 200 * 44300
|
||||
// Non-Business: $non_business_count * 72 * 16000
|
||||
$business_total = $business_count * 200 * 44300;
|
||||
$non_business_total = $non_business_count * 72 * 16000;
|
||||
$non_verified_total = $business_total + $non_business_total;
|
||||
|
||||
// Get other sum values using proper aggregation to handle multiple retributions
|
||||
$stats = PbgTask::leftJoin('pbg_task_retributions as ptr', 'pbg_task.uuid', '=', 'ptr.pbg_task_uid')
|
||||
->where('pbg_task.is_valid', true)
|
||||
->where('pbg_task.due_date', '>=', $year.'-02-01')
|
||||
->selectRaw("
|
||||
COUNT(CASE WHEN LOWER(TRIM(ptgs.status_verifikasi)) = 'selesai verifikasi' THEN 1 END) AS verified_count,
|
||||
SUM(CASE WHEN LOWER(TRIM(ptgs.status_verifikasi)) = 'selesai verifikasi' THEN ptr.nilai_retribusi_bangunan ELSE 0 END) AS verified_total,
|
||||
|
||||
COUNT(CASE WHEN LOWER(TRIM(ptgs.status_verifikasi)) != 'selesai verifikasi' OR ptgs.status_verifikasi IS NULL THEN 1 END) AS non_verified_count,
|
||||
SUM(CASE WHEN LOWER(TRIM(ptgs.status_verifikasi)) != 'selesai verifikasi' OR ptgs.status_verifikasi IS NULL THEN ptr.nilai_retribusi_bangunan ELSE 0 END) AS non_verified_total,
|
||||
|
||||
COUNT(CASE WHEN (LOWER(TRIM(ptgs.status_verifikasi)) != 'selesai verifikasi' OR ptgs.status_verifikasi IS NULL)
|
||||
AND LOWER(TRIM(pbg_task.function_type)) = 'sebagai tempat usaha' THEN 1 END) AS business_count,
|
||||
SUM(CASE WHEN (LOWER(TRIM(ptgs.status_verifikasi)) != 'selesai verifikasi' OR ptgs.status_verifikasi IS NULL)
|
||||
AND LOWER(TRIM(pbg_task.function_type)) = 'sebagai tempat usaha' THEN ptr.nilai_retribusi_bangunan ELSE 0 END) AS business_total,
|
||||
|
||||
COUNT(CASE WHEN (LOWER(TRIM(ptgs.status_verifikasi)) != 'selesai verifikasi' OR ptgs.status_verifikasi IS NULL)
|
||||
AND (LOWER(TRIM(pbg_task.function_type)) != 'sebagai tempat usaha' OR pbg_task.function_type IS NULL) THEN 1 END) AS non_business_count,
|
||||
SUM(CASE WHEN (LOWER(TRIM(ptgs.status_verifikasi)) != 'selesai verifikasi' OR ptgs.status_verifikasi IS NULL)
|
||||
AND (LOWER(TRIM(pbg_task.function_type)) != 'sebagai tempat usaha' OR pbg_task.function_type IS NULL) THEN ptr.nilai_retribusi_bangunan ELSE 0 END) AS non_business_total
|
||||
SUM(CASE WHEN pbg_task.status in (".implode(',', PbgTaskStatus::getVerified()).") THEN COALESCE(ptr.nilai_retribusi_bangunan, 0) ELSE 0 END) AS verified_total,
|
||||
SUM(CASE WHEN pbg_task.status in (".implode(',', PbgTaskStatus::getWaitingClickDpmptsp()).") THEN COALESCE(ptr.nilai_retribusi_bangunan, 0) ELSE 0 END) AS waiting_click_dpmptsp_total,
|
||||
SUM(CASE WHEN pbg_task.status in (".implode(',', PbgTaskStatus::getIssuanceRealizationPbg()).") THEN COALESCE(ptr.nilai_retribusi_bangunan, 0) ELSE 0 END) AS issuance_realization_pbg_total,
|
||||
SUM(CASE WHEN pbg_task.status in (".implode(',', PbgTaskStatus::getProcessInTechnicalOffice()).") THEN COALESCE(ptr.nilai_retribusi_bangunan, 0) ELSE 0 END) AS process_in_technical_office_total,
|
||||
SUM(CASE WHEN pbg_task.status in (".implode(',', PbgTaskStatus::getPotention()).") THEN COALESCE(ptr.nilai_retribusi_bangunan, 0) ELSE 0 END) AS potention_total,
|
||||
COUNT(CASE WHEN pbg_task.status in (".implode(',', PbgTaskStatus::getNonVerified()).") THEN 1 END) AS non_verified_tasks_count,
|
||||
COUNT(CASE WHEN pbg_task.status in (".implode(',', PbgTaskStatus::getNonVerified()).") AND ptr.nilai_retribusi_bangunan IS NOT NULL THEN 1 END) AS non_verified_with_retribution_count
|
||||
")
|
||||
->first();
|
||||
|
||||
// Assign Results
|
||||
$verified_count = $stats->verified_count ?? 0;
|
||||
$verified_total = $stats->verified_total ?? 0;
|
||||
$non_verified_count = $stats->non_verified_count ?? 0;
|
||||
$non_verified_total = $stats->non_verified_total ?? 0;
|
||||
$business_count = $stats->business_count ?? 0;
|
||||
$business_total = $stats->business_total ?? 0;
|
||||
$non_business_count = $stats->non_business_count ?? 0;
|
||||
$non_business_total = $stats->non_business_total ?? 0;
|
||||
\Log::info('Stats calculation result', [
|
||||
'business_count' => $business_count,
|
||||
'non_business_count' => $non_business_count,
|
||||
'business_total' => $business_total,
|
||||
'non_business_total' => $non_business_total,
|
||||
'non_verified_total' => $non_verified_total,
|
||||
'non_verified_tasks_count' => $stats->non_verified_tasks_count ?? 'NULL',
|
||||
'non_verified_with_retribution_count' => $stats->non_verified_with_retribution_count ?? 'NULL'
|
||||
]);
|
||||
|
||||
$query_potention = once(function () use ($year) {
|
||||
$query = PbgTask::leftJoin('pbg_task_retributions as ptr', 'pbg_task.uuid', '=', 'ptr.pbg_task_uid')
|
||||
->selectRaw('COUNT(DISTINCT pbg_task.id) as task_count, SUM(ptr.nilai_retribusi_bangunan) as total_retribution');
|
||||
|
||||
if ($year !== 'all') {
|
||||
$query->whereYear('pbg_task.task_created_at', (int) $year);
|
||||
}
|
||||
|
||||
return $query->first();
|
||||
});
|
||||
|
||||
$potention_count = $query_potention->task_count ?? 0;
|
||||
$potention_total = $query_potention->total_retribution ?? 0;
|
||||
|
||||
$query_spatial_plannings = once(function () use ($year) {
|
||||
$query = PbgTask::leftJoin('spatial_plannings as sp', 'pbg_task.document_number', '=', 'sp.number')
|
||||
->leftJoin('pbg_task_retributions as ptr', 'ptr.pbg_task_uid', '=', 'pbg_task.uuid')
|
||||
->selectRaw('
|
||||
CASE
|
||||
WHEN COUNT(DISTINCT sp.id) > 0 THEN COUNT(DISTINCT sp.id)
|
||||
ELSE (SELECT COUNT(*) FROM spatial_plannings)
|
||||
END as task_count,
|
||||
SUM(CASE WHEN sp.id IS NOT NULL AND ptr.id IS NOT NULL THEN ptr.nilai_retribusi_bangunan ELSE 0 END) as total_retribution
|
||||
');
|
||||
|
||||
if ($year !== 'all') {
|
||||
$query->whereYear('pbg_task.task_created_at', (int) $year);
|
||||
}
|
||||
|
||||
return $query->first();
|
||||
});
|
||||
|
||||
$spatial_planning_count = $query_spatial_plannings->task_count ?? 0;
|
||||
$spatial_planning_total = $query_spatial_plannings->total_retribution;
|
||||
|
||||
$potention_count -= $spatial_planning_count;
|
||||
$potention_total -= $spatial_planning_total;
|
||||
$service_google_sheet = app(ServiceGoogleSheet::class);
|
||||
|
||||
return self::create([
|
||||
'import_datasource_id' => $import_datasource_id,
|
||||
'spatial_count' => $spatial_planning_count,
|
||||
'spatial_sum' => $spatial_planning_total ?? 0.00,
|
||||
'potention_count' => $potention_count ?? 0,
|
||||
'potention_sum' => $potention_total ?? 0.00,
|
||||
'non_verified_count' => $non_verified_count ?? 0,
|
||||
'non_verified_sum' => $non_verified_total ?? 0.00,
|
||||
'verified_count' => $verified_count ?? 0,
|
||||
'verified_sum' => $verified_total ?? 0.00,
|
||||
'business_count' => $business_count ?? 0,
|
||||
'business_sum' => $business_total ?? 0.00,
|
||||
'non_business_count' => $non_business_count ?? 0,
|
||||
'non_business_sum' => $non_business_total ?? 0.00,
|
||||
'spatial_count' => $service_google_sheet->getSpatialPlanningWithCalculationCount() ?? 0,
|
||||
'spatial_sum' => $service_google_sheet->getSpatialPlanningCalculationSum() ?? 0.00,
|
||||
'potention_count' => $potention_count,
|
||||
'potention_sum' => ($stats->potention_total ?? 0),
|
||||
'non_verified_count' => $non_verified_count,
|
||||
'non_verified_sum' => $non_verified_total,
|
||||
'verified_count' => $verified_count,
|
||||
'verified_sum' => $stats->verified_total ?? 0.00,
|
||||
'business_count' => $business_count,
|
||||
'business_sum' => $business_total,
|
||||
'non_business_count' => $non_business_count,
|
||||
'non_business_sum' => $non_business_total,
|
||||
'year' => $year,
|
||||
'waiting_click_dpmptsp_count' => $data_setting['MENUNGGU_KLIK_DPMPTSP_COUNT'] ?? 0,
|
||||
'waiting_click_dpmptsp_sum' => $data_setting['MENUNGGU_KLIK_DPMPTSP_SUM'] ?? 0.00,
|
||||
'issuance_realization_pbg_count' => $data_setting['REALISASI_TERBIT_PBG_COUNT'] ?? 0,
|
||||
'issuance_realization_pbg_sum' => $data_setting['REALISASI_TERBIT_PBG_SUM'] ?? 0.00,
|
||||
'process_in_technical_office_count' => $data_setting['PROSES_DINAS_TEKNIS_COUNT'] ?? 0,
|
||||
'process_in_technical_office_sum' => $data_setting['PROSES_DINAS_TEKNIS_SUM'] ??0.00,
|
||||
'waiting_click_dpmptsp_count' => $waiting_click_dpmptsp_count,
|
||||
'waiting_click_dpmptsp_sum' => $stats->waiting_click_dpmptsp_total ?? 0.00,
|
||||
'issuance_realization_pbg_count' => $issuance_realization_pbg_count,
|
||||
'issuance_realization_pbg_sum' => $stats->issuance_realization_pbg_total ?? 0.00,
|
||||
'process_in_technical_office_count' => $process_in_technical_office_count,
|
||||
'process_in_technical_office_sum' => $stats->process_in_technical_office_total ?? 0.00,
|
||||
'business_rab_count' => $business_rab_count,
|
||||
'business_krk_count' => $business_krk_count,
|
||||
'business_dlh_count' => $business_dlh_count,
|
||||
'non_business_rab_count' => $non_business_rab_count,
|
||||
'non_business_krk_count' => $non_business_krk_count,
|
||||
'resume_type' => $resume_type,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
131
app/Models/BuildingType.php
Normal file
131
app/Models/BuildingType.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
class BuildingType extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'name',
|
||||
'parent_id',
|
||||
'level',
|
||||
'is_free',
|
||||
'is_active'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'level' => 'integer',
|
||||
'is_free' => 'boolean',
|
||||
'is_active' => 'boolean'
|
||||
];
|
||||
|
||||
/**
|
||||
* Parent relationship
|
||||
*/
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(BuildingType::class, 'parent_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Children relationship
|
||||
*/
|
||||
public function children(): HasMany
|
||||
{
|
||||
return $this->hasMany(BuildingType::class, 'parent_id')
|
||||
->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retribution indices relationship
|
||||
*/
|
||||
public function indices(): HasOne
|
||||
{
|
||||
return $this->hasOne(RetributionIndex::class, 'building_type_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculations relationship
|
||||
*/
|
||||
public function calculations(): HasMany
|
||||
{
|
||||
return $this->hasMany(RetributionCalculation::class, 'building_type_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: Active only
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: Parents only
|
||||
*/
|
||||
public function scopeParents($query)
|
||||
{
|
||||
return $query->whereNull('parent_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: Children only
|
||||
*/
|
||||
public function scopeChildren($query)
|
||||
{
|
||||
return $query->whereNotNull('parent_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: Non-free types
|
||||
*/
|
||||
public function scopeChargeable($query)
|
||||
{
|
||||
return $query->where('is_free', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if building type is free
|
||||
*/
|
||||
public function isFree(): bool
|
||||
{
|
||||
return $this->is_free;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a parent type
|
||||
*/
|
||||
public function isParent(): bool
|
||||
{
|
||||
return $this->parent_id === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a child type
|
||||
*/
|
||||
public function isChild(): bool
|
||||
{
|
||||
return $this->parent_id !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get complete data for calculation
|
||||
*/
|
||||
public function getCalculationData(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'code' => $this->code,
|
||||
'name' => $this->name,
|
||||
'coefficient' => $this->coefficient,
|
||||
'is_free' => $this->is_free,
|
||||
'indices' => $this->indices?->toArray(),
|
||||
'parent' => $this->parent?->only(['id', 'code', 'name'])
|
||||
];
|
||||
}
|
||||
}
|
||||
64
app/Models/CalculableRetribution.php
Normal file
64
app/Models/CalculableRetribution.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
class CalculableRetribution extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'retribution_calculation_id',
|
||||
'calculable_id',
|
||||
'calculable_type',
|
||||
'is_active',
|
||||
'assigned_at',
|
||||
'notes',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'assigned_at' => 'timestamp',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the owning calculable model (polymorphic)
|
||||
*/
|
||||
public function calculable(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the retribution calculation
|
||||
*/
|
||||
public function retributionCalculation(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(RetributionCalculation::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: Only active assignments
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: Only inactive assignments
|
||||
*/
|
||||
public function scopeInactive($query)
|
||||
{
|
||||
return $query->where('is_active', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: For specific calculable type
|
||||
*/
|
||||
public function scopeForType($query, string $type)
|
||||
{
|
||||
return $query->where('calculable_type', $type);
|
||||
}
|
||||
}
|
||||
55
app/Models/HeightIndex.php
Normal file
55
app/Models/HeightIndex.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class HeightIndex extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'floor_number',
|
||||
'height_index'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'floor_number' => 'integer',
|
||||
'height_index' => 'decimal:6'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get height index by floor number
|
||||
*/
|
||||
public static function getByFloor(int $floorNumber): ?HeightIndex
|
||||
{
|
||||
return self::where('floor_number', $floorNumber)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get height index value by floor number
|
||||
*/
|
||||
public static function getHeightIndexByFloor(int $floorNumber): float
|
||||
{
|
||||
$index = self::getByFloor($floorNumber);
|
||||
return $index ? (float) $index->height_index : 1.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all height indices as array
|
||||
*/
|
||||
public static function getAllMapping(): array
|
||||
{
|
||||
return self::orderBy('floor_number')
|
||||
->pluck('height_index', 'floor_number')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available floor numbers
|
||||
*/
|
||||
public static function getAvailableFloors(): array
|
||||
{
|
||||
return self::orderBy('floor_number')
|
||||
->pluck('floor_number')
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
57
app/Models/PbgStatus.php
Normal file
57
app/Models/PbgStatus.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Carbon\Carbon;
|
||||
class PbgStatus extends Model
|
||||
{
|
||||
protected $table = 'pbg_statuses';
|
||||
|
||||
protected $fillable = [
|
||||
'pbg_task_uuid',
|
||||
'status',
|
||||
'status_name',
|
||||
'slf_status',
|
||||
'slf_status_name',
|
||||
'due_date',
|
||||
'uid',
|
||||
'note',
|
||||
'file',
|
||||
'data_due_date',
|
||||
'data_created_at',
|
||||
'slf_data',
|
||||
];
|
||||
|
||||
public function pbgTask()
|
||||
{
|
||||
return $this->belongsTo(PbgTask::class, 'pbg_task_uuid', 'uuid');
|
||||
}
|
||||
|
||||
public static function createOrUpdateFromApi(array $apiResponse, string $pbgTaskUuid)
|
||||
{
|
||||
$data = $apiResponse['data'] ?? [];
|
||||
|
||||
return self::updateOrCreate(
|
||||
[
|
||||
'pbg_task_uuid' => $pbgTaskUuid,
|
||||
'status' => $apiResponse['status'], // key pencarian unik
|
||||
],
|
||||
[
|
||||
'status_name' => $apiResponse['status_name'] ?? null,
|
||||
'slf_status' => $apiResponse['slf_status'] ?? null,
|
||||
'slf_status_name' => $apiResponse['slf_status_name'] ?? null,
|
||||
'due_date' => $apiResponse['due_date'] ?? null,
|
||||
|
||||
// nested data
|
||||
'uid' => $data['uid'] ?? null,
|
||||
'note' => $data['note'] ?? null,
|
||||
'file' => $data['file'] ?? null,
|
||||
'data_due_date' => $data['due_date'] ?? null,
|
||||
'data_created_at' => isset($data['created_at']) ? Carbon::parse($data['created_at'])->format('Y-m-d H:i:s') : null,
|
||||
|
||||
'slf_data' => $apiResponse['slf_data'] ?? null,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,8 @@ class PbgTask extends Model
|
||||
'consultation_type',
|
||||
'due_date',
|
||||
'land_certificate_phase',
|
||||
'task_created_at'
|
||||
'task_created_at',
|
||||
'is_valid'
|
||||
];
|
||||
|
||||
public function pbg_task_retributions(){
|
||||
@@ -38,8 +39,12 @@ class PbgTask extends Model
|
||||
return $this->hasOne(PbgTaskIndexIntegrations::class, 'pbg_task_uid', 'uuid');
|
||||
}
|
||||
|
||||
public function pbg_task_detail(){
|
||||
return $this->hasOne(PbgTaskDetail::class, 'pbg_task_uid', 'uuid');
|
||||
}
|
||||
|
||||
public function googleSheet(){
|
||||
return $this->hasOne(PbgTaskGoogleSheet::class, 'no_registrasi', 'registration_number');
|
||||
return $this->hasOne(PbgTaskGoogleSheet::class, 'formatted_registration_number', 'registration_number');
|
||||
}
|
||||
|
||||
public function taskAssignments()
|
||||
@@ -50,4 +55,91 @@ class PbgTask extends Model
|
||||
public function attachments(){
|
||||
return $this->hasMany(PbgTaskAttachment::class, 'pbg_task_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data lists associated with this PBG task (One to Many)
|
||||
* One pbg_task can have many data lists
|
||||
*/
|
||||
public function dataLists()
|
||||
{
|
||||
return $this->hasMany(PbgTaskDetailDataList::class, 'pbg_task_uuid', 'uuid');
|
||||
}
|
||||
|
||||
public function pbg_status()
|
||||
{
|
||||
return $this->hasOne(PbgStatus::class, 'pbg_task_uuid', 'uuid');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get only data lists with files
|
||||
*/
|
||||
public function dataListsWithFiles()
|
||||
{
|
||||
return $this->hasMany(PbgTaskDetailDataList::class, 'pbg_task_uuid', 'uuid')
|
||||
->whereNotNull('file')
|
||||
->where('file', '!=', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data lists by status
|
||||
*/
|
||||
public function dataListsByStatus($status)
|
||||
{
|
||||
return $this->hasMany(PbgTaskDetailDataList::class, 'pbg_task_uuid', 'uuid')
|
||||
->where('status', $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data lists by data type
|
||||
*/
|
||||
public function dataListsByType($dataType)
|
||||
{
|
||||
return $this->hasMany(PbgTaskDetailDataList::class, 'pbg_task_uuid', 'uuid')
|
||||
->where('data_type', $dataType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update data lists from API response
|
||||
*/
|
||||
public function syncDataLists(array $dataLists): void
|
||||
{
|
||||
foreach ($dataLists as $listData) {
|
||||
PbgTaskDetailDataList::updateOrCreate(
|
||||
['uid' => $listData['uid']],
|
||||
[
|
||||
'name' => $listData['name'] ?? null,
|
||||
'description' => $listData['description'] ?? null,
|
||||
'status' => $listData['status'] ?? null,
|
||||
'status_name' => $listData['status_name'] ?? null,
|
||||
'data_type' => $listData['data_type'] ?? null,
|
||||
'data_type_name' => $listData['data_type_name'] ?? null,
|
||||
'file' => $listData['file'] ?? null,
|
||||
'note' => $listData['note'] ?? null,
|
||||
'pbg_task_uuid' => $this->uuid,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data lists count by status
|
||||
*/
|
||||
public function getDataListsCountByStatusAttribute()
|
||||
{
|
||||
return $this->dataLists()
|
||||
->selectRaw('status, COUNT(*) as count')
|
||||
->groupBy('status')
|
||||
->pluck('count', 'status');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data lists count by data type
|
||||
*/
|
||||
public function getDataListsCountByTypeAttribute()
|
||||
{
|
||||
return $this->dataLists()
|
||||
->selectRaw('data_type, COUNT(*) as count')
|
||||
->groupBy('data_type')
|
||||
->pluck('count', 'data_type');
|
||||
}
|
||||
}
|
||||
|
||||
432
app/Models/PbgTaskDetail.php
Normal file
432
app/Models/PbgTaskDetail.php
Normal file
@@ -0,0 +1,432 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class PbgTaskDetail extends Model
|
||||
{
|
||||
protected $table = 'pbg_task_details';
|
||||
|
||||
protected $fillable = [
|
||||
'pbg_task_uid',
|
||||
'uid',
|
||||
'nik',
|
||||
'type_card',
|
||||
'ownership',
|
||||
'owner_name',
|
||||
'ward_id',
|
||||
'ward_name',
|
||||
'district_id',
|
||||
'district_name',
|
||||
'regency_id',
|
||||
'regency_name',
|
||||
'province_id',
|
||||
'province_name',
|
||||
'address',
|
||||
'owner_email',
|
||||
'owner_phone',
|
||||
'user',
|
||||
'name',
|
||||
'email',
|
||||
'phone',
|
||||
'user_nik',
|
||||
'user_province_id',
|
||||
'user_province_name',
|
||||
'user_regency_id',
|
||||
'user_regency_name',
|
||||
'user_district_id',
|
||||
'user_district_name',
|
||||
'user_address',
|
||||
'status',
|
||||
'status_name',
|
||||
'slf_status',
|
||||
'slf_status_name',
|
||||
'sppst_status',
|
||||
'sppst_file',
|
||||
'sppst_status_name',
|
||||
'file_pbg',
|
||||
'file_pbg_date',
|
||||
'due_date',
|
||||
'start_date',
|
||||
'document_number',
|
||||
'registration_number',
|
||||
'function_type',
|
||||
'application_type',
|
||||
'application_type_name',
|
||||
'consultation_type',
|
||||
'condition',
|
||||
'prototype',
|
||||
'permanency',
|
||||
'building_type',
|
||||
'building_type_name',
|
||||
'building_purpose',
|
||||
'building_use',
|
||||
'occupancy',
|
||||
'name_building',
|
||||
'total_area',
|
||||
'area',
|
||||
'area_type',
|
||||
'height',
|
||||
'floor',
|
||||
'floor_area',
|
||||
'basement',
|
||||
'basement_height',
|
||||
'basement_area',
|
||||
'unit',
|
||||
'prev_retribution',
|
||||
'prev_pbg',
|
||||
'prev_total_area',
|
||||
'koefisien_dasar_bangunan',
|
||||
'koefisien_lantai_bangunan',
|
||||
'koefisien_lantai_hijau',
|
||||
'koefisien_tapak_basement',
|
||||
'ketinggian_bangunan',
|
||||
'jalan_arteri',
|
||||
'jalan_kolektor',
|
||||
'jalan_bangunan',
|
||||
'gsb',
|
||||
'kkr_number',
|
||||
'unit_data',
|
||||
'is_mbr',
|
||||
'code',
|
||||
'building_ward_id',
|
||||
'building_ward_name',
|
||||
'building_district_id',
|
||||
'building_district_name',
|
||||
'building_regency_id',
|
||||
'building_regency_name',
|
||||
'building_province_id',
|
||||
'building_province_name',
|
||||
'building_address',
|
||||
'latitude',
|
||||
'longitude',
|
||||
'building_photo',
|
||||
'pbg_parent',
|
||||
'api_created_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'unit_data' => 'array',
|
||||
'is_mbr' => 'boolean',
|
||||
'total_area' => 'decimal:2',
|
||||
'area' => 'decimal:2',
|
||||
'height' => 'decimal:2',
|
||||
'floor_area' => 'decimal:2',
|
||||
'basement_height' => 'decimal:2',
|
||||
'basement_area' => 'decimal:2',
|
||||
'prev_retribution' => 'decimal:2',
|
||||
'prev_total_area' => 'decimal:2',
|
||||
'koefisien_dasar_bangunan' => 'decimal:4',
|
||||
'koefisien_lantai_bangunan' => 'decimal:4',
|
||||
'koefisien_lantai_hijau' => 'decimal:4',
|
||||
'koefisien_tapak_basement' => 'decimal:4',
|
||||
'ketinggian_bangunan' => 'decimal:2',
|
||||
'gsb' => 'decimal:2',
|
||||
'latitude' => 'decimal:8',
|
||||
'longitude' => 'decimal:8',
|
||||
'file_pbg_date' => 'date',
|
||||
'due_date' => 'date',
|
||||
'start_date' => 'date',
|
||||
'api_created_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the PBG task that owns this detail
|
||||
*/
|
||||
public function pbgTask(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PbgTask::class, 'pbg_task_uid', 'uuid');
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Helper method to clean and convert latitude/longitude values
|
||||
*/
|
||||
private static function cleanCoordinate($value): ?float
|
||||
{
|
||||
if ($value === null || $value === '' || $value === '?' || $value === '-') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert to string and trim whitespace
|
||||
$stringValue = trim((string) $value);
|
||||
|
||||
// Check for common invalid values
|
||||
if (in_array($stringValue, ['', '?', '-', 'null', 'NULL', 'N/A', '0,'], true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove degree symbol and other non-numeric characters except minus and decimal point
|
||||
$cleaned = preg_replace('/[^\d.-]/', '', $stringValue);
|
||||
|
||||
// Check if cleaned value is empty or just a hyphen
|
||||
if ($cleaned === '' || $cleaned === '-' || $cleaned === '.') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate if it's a valid number and within reasonable coordinate bounds
|
||||
if (is_numeric($cleaned)) {
|
||||
$coordinate = (float) $cleaned;
|
||||
|
||||
// Basic validation for reasonable coordinate ranges
|
||||
// Latitude: -90 to 90, Longitude: -180 to 180
|
||||
if ($coordinate >= -180 && $coordinate <= 180) {
|
||||
return $coordinate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to clean and convert integer values
|
||||
*/
|
||||
private static function cleanIntegerValue($value): int
|
||||
{
|
||||
if ($value === null || $value === '' || $value === '?') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Convert to string and trim whitespace
|
||||
$stringValue = trim((string) $value);
|
||||
|
||||
// Check for common invalid values
|
||||
if (in_array($stringValue, ['', '?', '-', 'null', 'NULL', 'N/A'], true)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Remove any non-numeric characters except minus
|
||||
$cleaned = preg_replace('/[^\d-]/', '', $stringValue);
|
||||
|
||||
// Check if cleaned value is empty or just invalid characters
|
||||
if ($cleaned === '' || $cleaned === '-') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Validate if it's a valid number
|
||||
if (is_numeric($cleaned)) {
|
||||
return (int) $cleaned;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to clean and convert numeric values
|
||||
*/
|
||||
private static function cleanNumericValue($value, bool $nullable = false): ?float
|
||||
{
|
||||
if ($value === null || $value === '' || $value === '?') {
|
||||
return $nullable ? null : 0;
|
||||
}
|
||||
|
||||
// Convert to string and trim whitespace
|
||||
$stringValue = trim((string) $value);
|
||||
|
||||
// Check for common invalid values
|
||||
if (in_array($stringValue, ['', '?', '-', 'null', 'NULL', 'N/A'], true)) {
|
||||
return $nullable ? null : 0;
|
||||
}
|
||||
|
||||
// Remove any non-numeric characters except minus and decimal point
|
||||
$cleaned = preg_replace('/[^\d.-]/', '', $stringValue);
|
||||
|
||||
// Check if cleaned value is empty or just invalid characters
|
||||
if ($cleaned === '' || $cleaned === '-' || $cleaned === '.') {
|
||||
return $nullable ? null : 0;
|
||||
}
|
||||
|
||||
// Validate if it's a valid number
|
||||
if (is_numeric($cleaned)) {
|
||||
return (float) $cleaned;
|
||||
}
|
||||
|
||||
return $nullable ? null : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to handle date parsing with fallback
|
||||
*/
|
||||
private static function parseDate($date): ?string
|
||||
{
|
||||
if (!$date || $date === '?' || $date === 'null') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Carbon::parse($date)->format('Y-m-d');
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to handle datetime parsing with fallback
|
||||
*/
|
||||
private static function parseDateTime($datetime): ?string
|
||||
{
|
||||
if (!$datetime || $datetime === '?' || $datetime === 'null') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Carbon::parse($datetime)->format('Y-m-d H:i:s');
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update PbgTaskDetail from API response
|
||||
*/
|
||||
public static function createFromApiResponse(array $data, string $pbgTaskUuid): self
|
||||
{
|
||||
$detailData = [
|
||||
// Foreign key relationship - string, required
|
||||
'pbg_task_uid' => $pbgTaskUuid,
|
||||
|
||||
// Basic information
|
||||
'uid' => $data['uid'] ?? "N/A", // string, unique, required
|
||||
'nik' => isset($data['nik']) && $data['nik'] !== '' && $data['nik'] !== '?' ? $data['nik'] : null, // string, nullable
|
||||
'type_card' => isset($data['type_card']) && $data['type_card'] !== '' && $data['type_card'] !== '?' ? $data['type_card'] : null, // string, nullable
|
||||
'ownership' => $data['ownership'] ?? null, // string, nullable
|
||||
'owner_name' => $data['owner_name'] ?? "N/A", // string, required
|
||||
|
||||
// Owner location information - all required
|
||||
'ward_id' => self::cleanIntegerValue($data['ward_id'] ?? 0), // bigInteger, required
|
||||
'ward_name' => $data['ward_name'] ?? "N/A", // string, required
|
||||
'district_id' => self::cleanIntegerValue($data['district_id'] ?? 0), // integer, required
|
||||
'district_name' => $data['district_name'] ?? "N/A", // string, required
|
||||
'regency_id' => self::cleanIntegerValue($data['regency_id'] ?? 0), // integer, required
|
||||
'regency_name' => $data['regency_name'] ?? "N/A", // string, required
|
||||
'province_id' => self::cleanIntegerValue($data['province_id'] ?? 0), // integer, required
|
||||
'province_name' => $data['province_name'] ?? "N/A", // string, required
|
||||
'address' => $data['address'] ?? "N/A", // text, required
|
||||
|
||||
// Owner contact information - required
|
||||
'owner_email' => $data['owner_email'] ?? "N/A", // string, required
|
||||
'owner_phone' => $data['owner_phone'] ?? "N/A", // string, required
|
||||
|
||||
// User information - all required
|
||||
'user' => self::cleanIntegerValue($data['user'] ?? 0), // integer, required
|
||||
'name' => $data['name'] ?? "N/A", // string, required
|
||||
'email' => $data['email'] ?? "N/A", // string, required
|
||||
'phone' => $data['phone'] ?? "N/A", // string, required
|
||||
'user_nik' => $data['user_nik'] ?? "N/A", // string, required
|
||||
|
||||
// User location information - all required
|
||||
'user_province_id' => self::cleanIntegerValue($data['user_province_id'] ?? 0), // integer, required
|
||||
'user_province_name' => $data['user_province_name'] ?? "N/A", // string, required
|
||||
'user_regency_id' => self::cleanIntegerValue($data['user_regency_id'] ?? 0), // integer, required
|
||||
'user_regency_name' => $data['user_regency_name'] ?? "N/A", // string, required
|
||||
'user_district_id' => self::cleanIntegerValue($data['user_district_id'] ?? 0), // integer, required
|
||||
'user_district_name' => $data['user_district_name'] ?? "N/A", // string, required
|
||||
'user_address' => $data['user_address'] ?? "N/A", // text, required
|
||||
|
||||
// Status information
|
||||
'status' => self::cleanIntegerValue($data['status'] ?? 0), // integer, required
|
||||
'status_name' => $data['status_name'] ?? "N/A", // string, required
|
||||
'slf_status' => isset($data['slf_status']) && is_numeric($data['slf_status']) ? (int) $data['slf_status'] : null, // integer, nullable
|
||||
'slf_status_name' => $data['slf_status_name'] ?? null, // string, nullable
|
||||
'sppst_status' => self::cleanIntegerValue($data['sppst_status'] ?? 0), // integer, required
|
||||
'sppst_file' => $data['sppst_file'] ?? null, // string, nullable
|
||||
'sppst_status_name' => $data['sppst_status_name'] ?? "N/A", // string, required
|
||||
|
||||
// Files and documents
|
||||
'file_pbg' => $data['file_pbg'] ?? null, // string, nullable
|
||||
'file_pbg_date' => self::parseDate($data['file_pbg_date'] ?? null), // date, nullable
|
||||
'due_date' => self::parseDate($data['due_date'] ?? null), // date, nullable
|
||||
'start_date' => self::parseDate($data['start_date'] ?? null) ?? now()->format('Y-m-d'), // date, required
|
||||
'document_number' => $data['document_number'] ?? null, // string, nullable
|
||||
'registration_number' => $data['registration_number'] ?? "N/A", // string, required
|
||||
|
||||
// Application information - all nullable
|
||||
'function_type' => $data['function_type'] ?? null,
|
||||
'application_type' => $data['application_type'] ?? null,
|
||||
'application_type_name' => $data['application_type_name'] ?? null,
|
||||
'consultation_type' => $data['consultation_type'] ?? null,
|
||||
'condition' => $data['condition'] ?? null,
|
||||
'prototype' => $data['prototype'] ?? null,
|
||||
'permanency' => $data['permanency'] ?? null,
|
||||
|
||||
// Building information - all nullable
|
||||
'building_type' => isset($data['building_type']) && is_numeric($data['building_type']) ? (int) $data['building_type'] : null, // integer, nullable
|
||||
'building_type_name' => $data['building_type_name'] ?? null,
|
||||
'building_purpose' => $data['building_purpose'] ?? null,
|
||||
'building_use' => $data['building_use'] ?? null,
|
||||
'occupancy' => $data['occupancy'] ?? null,
|
||||
'name_building' => $data['name_building'] ?? null,
|
||||
|
||||
// Building dimensions and specifications
|
||||
'total_area' => self::cleanNumericValue($data['total_area'] ?? 0), // decimal(10,2), required
|
||||
'area' => self::cleanNumericValue($data['area'] ?? null, true), // decimal(10,2), nullable
|
||||
'area_type' => $data['area_type'] ?? null, // string, nullable
|
||||
'height' => self::cleanNumericValue($data['height'] ?? 0), // decimal(8,2), required
|
||||
'floor' => self::cleanIntegerValue($data['floor'] ?? 0), // integer, required
|
||||
'floor_area' => self::cleanNumericValue($data['floor_area'] ?? null, true), // decimal(10,2), nullable
|
||||
'basement' => isset($data['basement']) && $data['basement'] !== '' && $data['basement'] !== '?' ? $data['basement'] : null, // string, nullable
|
||||
'basement_height' => self::cleanNumericValue($data['basement_height'] ?? null, true), // decimal(8,2), nullable
|
||||
'basement_area' => self::cleanNumericValue($data['basement_area'] ?? 0), // decimal(10,2), required
|
||||
'unit' => isset($data['unit']) && is_numeric($data['unit']) ? (int) $data['unit'] : null, // integer, nullable
|
||||
|
||||
// Previous information
|
||||
'prev_retribution' => self::cleanNumericValue($data['prev_retribution'] ?? null, true), // decimal(15,2), nullable
|
||||
'prev_pbg' => $data['prev_pbg'] ?? null, // string, nullable
|
||||
'prev_total_area' => self::cleanNumericValue($data['prev_total_area'] ?? null, true), // decimal(10,2), nullable
|
||||
|
||||
// Coefficients - all nullable, decimal(8,4)
|
||||
'koefisien_dasar_bangunan' => self::cleanNumericValue($data['koefisien_dasar_bangunan'] ?? null, true),
|
||||
'koefisien_lantai_bangunan' => self::cleanNumericValue($data['koefisien_lantai_bangunan'] ?? null, true),
|
||||
'koefisien_lantai_hijau' => self::cleanNumericValue($data['koefisien_lantai_hijau'] ?? null, true),
|
||||
'koefisien_tapak_basement' => self::cleanNumericValue($data['koefisien_tapak_basement'] ?? null, true),
|
||||
'ketinggian_bangunan' => self::cleanNumericValue($data['ketinggian_bangunan'] ?? null, true), // decimal(8,2), nullable
|
||||
|
||||
// Road information - all nullable
|
||||
'jalan_arteri' => $data['jalan_arteri'] ?? null,
|
||||
'jalan_kolektor' => $data['jalan_kolektor'] ?? null,
|
||||
'jalan_bangunan' => $data['jalan_bangunan'] ?? null,
|
||||
'gsb' => self::cleanNumericValue($data['gsb'] ?? null, true), // decimal(8,2), nullable
|
||||
'kkr_number' => $data['kkr_number'] ?? null, // string, nullable
|
||||
|
||||
// Unit data as JSON - nullable
|
||||
'unit_data' => $data['unit_data'] ?? null,
|
||||
|
||||
// Additional flags
|
||||
'is_mbr' => (bool) ($data['is_mbr'] ?? false), // boolean, default false
|
||||
'code' => $data['code'] ?? "N/A", // string, required
|
||||
|
||||
// Building location information - all required
|
||||
'building_ward_id' => self::cleanIntegerValue($data['building_ward_id'] ?? 0), // bigInteger, required
|
||||
'building_ward_name' => $data['building_ward_name'] ?? "N/A", // string, required
|
||||
'building_district_id' => self::cleanIntegerValue($data['building_district_id'] ?? 0), // integer, required
|
||||
'building_district_name' => $data['building_district_name'] ?? "N/A", // string, required
|
||||
'building_regency_id' => self::cleanIntegerValue($data['building_regency_id'] ?? 0), // integer, required
|
||||
'building_regency_name' => $data['building_regency_name'] ?? "N/A", // string, required
|
||||
'building_province_id' => self::cleanIntegerValue($data['building_province_id'] ?? 0), // integer, required
|
||||
'building_province_name' => $data['building_province_name'] ?? "N/A", // string, required
|
||||
'building_address' => $data['building_address'] ?? "N/A", // text, required
|
||||
|
||||
// Coordinates - decimal(15,8), nullable
|
||||
'latitude' => self::cleanCoordinate($data['latitude'] ?? null),
|
||||
'longitude' => self::cleanCoordinate($data['longitude'] ?? null),
|
||||
|
||||
// Additional files - nullable
|
||||
'building_photo' => $data['building_photo'] ?? null,
|
||||
'pbg_parent' => $data['pbg_parent'] ?? null,
|
||||
|
||||
// Original created_at from API - nullable
|
||||
'api_created_at' => self::parseDateTime($data['created_at'] ?? null),
|
||||
];
|
||||
|
||||
return static::updateOrCreate(
|
||||
['uid' => $data['uid']],
|
||||
$detailData
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
161
app/Models/PbgTaskDetailDataList.php
Normal file
161
app/Models/PbgTaskDetailDataList.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PbgTaskDetailDataList extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'pbg_task_detail_data_lists';
|
||||
|
||||
protected $fillable = [
|
||||
'uid',
|
||||
'name',
|
||||
'description',
|
||||
'status',
|
||||
'status_name',
|
||||
'data_type',
|
||||
'data_type_name',
|
||||
'file',
|
||||
'note',
|
||||
'pbg_task_uuid',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'status' => 'integer',
|
||||
'data_type' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Relationship to PbgTask (Many to One)
|
||||
* Many data lists belong to one pbg_task
|
||||
*/
|
||||
public function pbgTask()
|
||||
{
|
||||
return $this->belongsTo(PbgTask::class, 'pbg_task_uuid', 'uuid');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full file path
|
||||
*/
|
||||
public function getFilePathAttribute()
|
||||
{
|
||||
return $this->file ? storage_path('app/public/' . $this->file) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file URL
|
||||
*/
|
||||
public function getFileUrlAttribute()
|
||||
{
|
||||
return $this->file ? asset('storage/' . $this->file) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file exists
|
||||
*/
|
||||
public function hasFile()
|
||||
{
|
||||
return !empty($this->file) && file_exists($this->getFilePathAttribute());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status badge color based on status
|
||||
*/
|
||||
public function getStatusBadgeAttribute()
|
||||
{
|
||||
return match($this->status) {
|
||||
1 => 'success', // Sesuai
|
||||
0 => 'danger', // Tidak Sesuai
|
||||
default => 'secondary'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: Filter by status
|
||||
*/
|
||||
public function scopeByStatus($query, $status)
|
||||
{
|
||||
return $query->where('status', $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: Filter by data type
|
||||
*/
|
||||
public function scopeByDataType($query, $dataType)
|
||||
{
|
||||
return $query->where('data_type', $dataType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: With files only
|
||||
*/
|
||||
public function scopeWithFiles($query)
|
||||
{
|
||||
return $query->whereNotNull('file')->where('file', '!=', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: Search by name or description
|
||||
*/
|
||||
public function scopeSearch($query, $search)
|
||||
{
|
||||
return $query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'LIKE', "%{$search}%")
|
||||
->orWhere('description', 'LIKE', "%{$search}%")
|
||||
->orWhere('status_name', 'LIKE', "%{$search}%")
|
||||
->orWhere('data_type_name', 'LIKE', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file extension from file path
|
||||
*/
|
||||
public function getFileExtensionAttribute()
|
||||
{
|
||||
if (!$this->file) {
|
||||
return null;
|
||||
}
|
||||
return strtoupper(pathinfo($this->file, PATHINFO_EXTENSION));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filename from file path
|
||||
*/
|
||||
public function getFileNameAttribute()
|
||||
{
|
||||
if (!$this->file) {
|
||||
return null;
|
||||
}
|
||||
return basename($this->file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted created date
|
||||
*/
|
||||
public function getFormattedCreatedAtAttribute()
|
||||
{
|
||||
return $this->created_at ? $this->created_at->format('d M Y, H:i') : '-';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get truncated description
|
||||
*/
|
||||
public function getTruncatedDescriptionAttribute()
|
||||
{
|
||||
return $this->description ? \Str::limit($this->description, 80) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get truncated note
|
||||
*/
|
||||
public function getTruncatedNoteAttribute()
|
||||
{
|
||||
return $this->note ? \Str::limit($this->note, 100) : null;
|
||||
}
|
||||
}
|
||||
136
app/Models/PbgTaskPayment.php
Normal file
136
app/Models/PbgTaskPayment.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class PbgTaskPayment extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'pbg_task_id',
|
||||
'pbg_task_uid',
|
||||
// mapped fields
|
||||
'row_no',
|
||||
'consultation_type',
|
||||
'source_registration_number',
|
||||
'owner_name',
|
||||
'building_location',
|
||||
'building_function',
|
||||
'building_name',
|
||||
'application_date_raw',
|
||||
'verification_status',
|
||||
'application_status',
|
||||
'owner_address',
|
||||
'owner_phone',
|
||||
'owner_email',
|
||||
'note_date_raw',
|
||||
'document_shortage_note',
|
||||
'image_url',
|
||||
'krk_kkpr',
|
||||
'krk_number',
|
||||
'lh',
|
||||
'ska',
|
||||
'remarks',
|
||||
'helpdesk',
|
||||
'person_in_charge',
|
||||
'pbg_operator',
|
||||
'ownership',
|
||||
'taru_potential',
|
||||
'agency_validation',
|
||||
'retribution_category',
|
||||
'ba_tpt_number',
|
||||
'ba_tpt_date_raw',
|
||||
'ba_tpa_number',
|
||||
'ba_tpa_date_raw',
|
||||
'skrd_number',
|
||||
'skrd_date_raw',
|
||||
'ptsp_status',
|
||||
'issued_status',
|
||||
'payment_date_raw',
|
||||
'sts_format',
|
||||
'issuance_year',
|
||||
'current_year',
|
||||
'village',
|
||||
'district',
|
||||
'building_area',
|
||||
'building_height',
|
||||
'floor_count',
|
||||
'unit_count',
|
||||
'proposed_retribution',
|
||||
'retribution_total_simbg',
|
||||
'retribution_total_pad',
|
||||
'penalty_amount',
|
||||
'business_category',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'application_date_raw' => 'date',
|
||||
'note_date_raw' => 'date',
|
||||
'ba_tpt_date_raw' => 'date',
|
||||
'ba_tpa_date_raw' => 'date',
|
||||
'skrd_date_raw' => 'date',
|
||||
'payment_date_raw' => 'date',
|
||||
'issuance_year' => 'integer',
|
||||
'current_year' => 'integer',
|
||||
'floor_count' => 'integer',
|
||||
'unit_count' => 'integer',
|
||||
'building_area' => 'decimal:2',
|
||||
'building_height' => 'decimal:2',
|
||||
'proposed_retribution' => 'decimal:2',
|
||||
'retribution_total_simbg' => 'decimal:2',
|
||||
'retribution_total_pad' => 'decimal:2',
|
||||
'penalty_amount' => 'decimal:2'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the PBG task that owns this payment
|
||||
*/
|
||||
public function pbgTask(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PbgTask::class, 'pbg_task_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean and convert registration number for matching
|
||||
*/
|
||||
public static function cleanRegistrationNumber(string $registrationNumber): string
|
||||
{
|
||||
return trim($registrationNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert pad amount string to decimal
|
||||
*/
|
||||
public static function convertPadAmount(?string $padAmount): float
|
||||
{
|
||||
if (empty($padAmount)) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Remove dots (thousands separator) and convert to float
|
||||
$cleaned = str_replace('.', '', $padAmount);
|
||||
$cleaned = str_replace(',', '.', $cleaned); // Handle comma as decimal separator if present
|
||||
|
||||
return is_numeric($cleaned) ? (float) $cleaned : 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert date string to proper format
|
||||
*/
|
||||
public static function convertPaymentDate(?string $dateString): ?string
|
||||
{
|
||||
if (empty($dateString)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Carbon::parse($dateString)->format('Y-m-d');
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
170
app/Models/RetributionCalculation.php
Normal file
170
app/Models/RetributionCalculation.php
Normal file
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class RetributionCalculation extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'calculation_id',
|
||||
'building_type_id',
|
||||
'floor_number',
|
||||
'building_area',
|
||||
'retribution_amount',
|
||||
'calculation_detail',
|
||||
'calculated_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'building_area' => 'decimal:2',
|
||||
'retribution_amount' => 'decimal:2',
|
||||
'calculation_detail' => 'array',
|
||||
'calculated_at' => 'timestamp',
|
||||
'floor_number' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the building type
|
||||
*/
|
||||
public function buildingType(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(BuildingType::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all calculable assignments
|
||||
*/
|
||||
public function calculableRetributions(): HasMany
|
||||
{
|
||||
return $this->hasMany(CalculableRetribution::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active assignments only
|
||||
*/
|
||||
public function activeAssignments(): HasMany
|
||||
{
|
||||
return $this->hasMany(CalculableRetribution::class)->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique calculation ID
|
||||
*/
|
||||
public static function generateCalculationId(): string
|
||||
{
|
||||
$maxAttempts = 10;
|
||||
$attempt = 0;
|
||||
|
||||
do {
|
||||
// Use microseconds for better uniqueness but keep within 20 char limit
|
||||
// Format: CALC-YYYYMMDD-XXXXX (20 chars exactly)
|
||||
$microseconds = (int) (microtime(true) * 1000) % 100000; // 5 digits max
|
||||
$id = 'CALC-' . date('Ymd') . '-' . str_pad($microseconds, 5, '0', STR_PAD_LEFT);
|
||||
|
||||
// Check if ID already exists
|
||||
if (!self::where('calculation_id', $id)->exists()) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$attempt++;
|
||||
// Add small delay to ensure different microsecond values
|
||||
usleep(1000); // 1ms delay
|
||||
|
||||
} while ($attempt < $maxAttempts);
|
||||
|
||||
// Fallback to random 5-digit number if all attempts fail
|
||||
for ($i = 0; $i < 100; $i++) {
|
||||
$random = mt_rand(10000, 99999);
|
||||
$id = 'CALC-' . date('Ymd') . '-' . $random;
|
||||
|
||||
if (!self::where('calculation_id', $id)->exists()) {
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback - use current timestamp seconds
|
||||
return 'CALC-' . date('Ymd') . '-' . str_pad(time() % 100000, 5, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot method to auto-generate calculation_id
|
||||
*/
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($model) {
|
||||
if (empty($model->calculation_id)) {
|
||||
$model->calculation_id = self::generateCalculationId();
|
||||
}
|
||||
if (empty($model->calculated_at)) {
|
||||
$model->calculated_at = now();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if calculation is being used
|
||||
*/
|
||||
public function isInUse(): bool
|
||||
{
|
||||
return $this->activeAssignments()->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get calculation summary
|
||||
*/
|
||||
public function getSummary(): array
|
||||
{
|
||||
return [
|
||||
'calculation_id' => $this->calculation_id,
|
||||
'building_type' => $this->buildingType->name ?? 'Unknown',
|
||||
'floor_number' => $this->floor_number,
|
||||
'building_area' => $this->building_area,
|
||||
'retribution_amount' => $this->retribution_amount,
|
||||
'calculated_at' => $this->calculated_at->format('Y-m-d H:i:s'),
|
||||
'in_use' => $this->isInUse(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new calculation
|
||||
*/
|
||||
public static function createCalculation(
|
||||
int $buildingTypeId,
|
||||
int $floorNumber,
|
||||
float $buildingArea,
|
||||
float $retributionAmount,
|
||||
array $calculationDetail
|
||||
): self {
|
||||
return self::create([
|
||||
'calculation_id' => self::generateCalculationId(),
|
||||
'building_type_id' => $buildingTypeId,
|
||||
'floor_number' => $floorNumber,
|
||||
'building_area' => $buildingArea,
|
||||
'retribution_amount' => $retributionAmount,
|
||||
'calculation_detail' => $calculationDetail,
|
||||
'calculated_at' => Carbon::now()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted retribution amount
|
||||
*/
|
||||
public function getFormattedAmount(): string
|
||||
{
|
||||
return 'Rp ' . number_format($this->retribution_amount, 2, ',', '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get calculation breakdown
|
||||
*/
|
||||
public function getCalculationBreakdown(): array
|
||||
{
|
||||
return $this->calculation_detail ?? [];
|
||||
}
|
||||
}
|
||||
50
app/Models/RetributionConfig.php
Normal file
50
app/Models/RetributionConfig.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class RetributionConfig extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'key',
|
||||
'value',
|
||||
'description',
|
||||
'is_active'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'value' => 'decimal:2',
|
||||
'is_active' => 'boolean'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get config value by key
|
||||
*/
|
||||
public static function getValue(string $key, float $default = 0.0): float
|
||||
{
|
||||
$config = self::where('key', $key)->where('is_active', true)->first();
|
||||
return $config ? (float) $config->value : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active configs as array
|
||||
*/
|
||||
public static function getAllActive(): array
|
||||
{
|
||||
return self::where('is_active', true)
|
||||
->pluck('value', 'key')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update config value
|
||||
*/
|
||||
public static function updateValue(string $key, float $value): bool
|
||||
{
|
||||
return self::updateOrCreate(
|
||||
['key' => $key],
|
||||
['value' => $value, 'is_active' => true]
|
||||
);
|
||||
}
|
||||
}
|
||||
57
app/Models/RetributionIndex.php
Normal file
57
app/Models/RetributionIndex.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class RetributionIndex extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'building_type_id',
|
||||
'coefficient',
|
||||
'ip_permanent',
|
||||
'ip_complexity',
|
||||
'locality_index',
|
||||
'infrastructure_factor',
|
||||
'is_active'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'coefficient' => 'decimal:4',
|
||||
'ip_permanent' => 'decimal:4',
|
||||
'ip_complexity' => 'decimal:4',
|
||||
'locality_index' => 'decimal:4',
|
||||
'infrastructure_factor' => 'decimal:4',
|
||||
'is_active' => 'boolean'
|
||||
];
|
||||
|
||||
/**
|
||||
* Building type relationship
|
||||
*/
|
||||
public function buildingType(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(BuildingType::class, 'building_type_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: Active only
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all indices as array
|
||||
*/
|
||||
public function getIndicesArray(): array
|
||||
{
|
||||
return [
|
||||
'ip_permanent' => $this->ip_permanent,
|
||||
'ip_complexity' => $this->ip_complexity,
|
||||
'locality_index' => $this->locality_index,
|
||||
'infrastructure_factor' => $this->infrastructure_factor
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasRetributionCalculation;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
|
||||
/**
|
||||
* Class SpatialPlanning
|
||||
*
|
||||
@@ -23,6 +25,7 @@ use Illuminate\Database\Eloquent\Model;
|
||||
*/
|
||||
class SpatialPlanning extends Model
|
||||
{
|
||||
use HasRetributionCalculation;
|
||||
|
||||
protected $perPage = 20;
|
||||
|
||||
@@ -31,7 +34,209 @@ class SpatialPlanning extends Model
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = ['name', 'kbli', 'activities', 'area', 'location', 'number', 'date'];
|
||||
protected $fillable = ['name', 'kbli', 'activities', 'area', 'location', 'number', 'date', 'no_tapak', 'no_skkl', 'no_ukl', 'building_function', 'sub_building_function', 'number_of_floors', 'land_area', 'site_bcr', 'is_terbit'];
|
||||
|
||||
protected $casts = [
|
||||
'area' => 'decimal:6',
|
||||
'land_area' => 'decimal:6',
|
||||
'site_bcr' => 'decimal:6',
|
||||
'number_of_floors' => 'integer',
|
||||
'date' => 'date',
|
||||
'is_terbit' => 'boolean'
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
'calculated_retribution',
|
||||
'formatted_retribution',
|
||||
'is_business_type',
|
||||
'calculation_details',
|
||||
'old_calculation_amount',
|
||||
'calculation_source'
|
||||
];
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get building function text for detection
|
||||
*/
|
||||
public function getBuildingFunctionText(): string
|
||||
{
|
||||
return $this->building_function ?? $this->activities ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get area for calculation (prioritize area, fallback to land_area)
|
||||
*/
|
||||
public function getCalculationArea(): float
|
||||
{
|
||||
return (float) ($this->area ?? $this->land_area ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get calculated retribution amount
|
||||
* Priority: Manual calculation (new formula) > Active calculation (old system)
|
||||
*/
|
||||
public function getCalculatedRetributionAttribute(): float
|
||||
{
|
||||
try {
|
||||
// PRIORITY 1: Use new manual formula (LUAS LAHAN × BCR × HARGA SATUAN)
|
||||
$manualCalculation = $this->calculateRetributionManually();
|
||||
|
||||
// If manual calculation is valid (> 0), use it
|
||||
if ($manualCalculation > 0) {
|
||||
return $manualCalculation;
|
||||
}
|
||||
|
||||
// PRIORITY 2: Fallback to active retribution calculation if exists
|
||||
$activeCalculation = $this->activeRetributionCalculation;
|
||||
|
||||
if ($activeCalculation && $activeCalculation->retributionCalculation) {
|
||||
return (float) $activeCalculation->retributionCalculation->retribution_amount;
|
||||
}
|
||||
|
||||
// PRIORITY 3: Return 0 if nothing works
|
||||
return 0.0;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::warning('Failed to calculate retribution for SpatialPlanning ID: ' . $this->id, [
|
||||
'error' => $e->getMessage(),
|
||||
'spatial_planning' => $this->toArray()
|
||||
]);
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual calculation based on area and building function
|
||||
* Formula: LUAS LAHAN × BCR × HARGA SATUAN
|
||||
* NON USAHA: 16,000 per m2
|
||||
* USAHA: 44,300 per m2
|
||||
*/
|
||||
private function calculateRetributionManually(): float
|
||||
{
|
||||
// Get land area (luas lahan)
|
||||
$landArea = (float) ($this->land_area ?? 0);
|
||||
|
||||
// Get BCR (Building Coverage Ratio) - convert from percentage to decimal
|
||||
$bcrPercentage = (float) ($this->site_bcr ?? 0);
|
||||
$bcr = $bcrPercentage / 100; // Convert percentage to decimal (24.49% -> 0.2449)
|
||||
|
||||
if ($landArea <= 0 || $bcr <= 0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Determine if this is business (USAHA) or non-business (NON USAHA)
|
||||
$isBusiness = $this->isBusinessType();
|
||||
|
||||
// Set unit price based on business type
|
||||
$unitPrice = $isBusiness ? 44300 : 16000;
|
||||
|
||||
// Calculate: LUAS LAHAN × BCR (as decimal) × HARGA SATUAN
|
||||
$calculatedAmount = $landArea * $bcr * $unitPrice;
|
||||
|
||||
return $calculatedAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this spatial planning is for business purposes
|
||||
*/
|
||||
private function isBusinessType(): bool
|
||||
{
|
||||
$buildingFunction = strtolower($this->building_function ?? $this->activities ?? '');
|
||||
|
||||
// Business keywords
|
||||
$businessKeywords = [
|
||||
'usaha', 'dagang', 'perdagangan', 'komersial', 'commercial', 'bisnis', 'business',
|
||||
'toko', 'warung', 'pasar', 'kios', 'mall', 'plaza', 'supermarket', 'department',
|
||||
'hotel', 'resort', 'restoran', 'restaurant', 'cafe', 'kantor', 'perkantoran', 'office',
|
||||
'industri', 'pabrik', 'gudang', 'warehouse', 'manufacturing', 'produksi',
|
||||
'bengkel', 'workshop', 'showroom', 'dealer', 'apotek', 'pharmacy', 'klinik swasta',
|
||||
'rumah sakit swasta', 'bank', 'atm', 'money changer', 'asuransi', 'leasing',
|
||||
'rental', 'sewa', 'jasa', 'service', 'salon', 'spa', 'fitness', 'gym',
|
||||
'tempat usaha', 'fungsi usaha', 'kegiatan usaha', 'bangunan usaha'
|
||||
];
|
||||
|
||||
// Check if any business keyword is found
|
||||
foreach ($businessKeywords as $keyword) {
|
||||
if (str_contains($buildingFunction, $keyword)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Non-business (default)
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted retribution amount for display
|
||||
*/
|
||||
public function getFormattedRetributionAttribute(): string
|
||||
{
|
||||
$amount = $this->calculated_retribution;
|
||||
return number_format($amount, 0, ',', '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is business type
|
||||
*/
|
||||
public function getIsBusinessTypeAttribute(): bool
|
||||
{
|
||||
return $this->isBusinessType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get calculation details for transparency
|
||||
*/
|
||||
public function getCalculationDetailsAttribute(): array
|
||||
{
|
||||
$landArea = (float) ($this->land_area ?? 0);
|
||||
$bcrPercentage = (float) ($this->site_bcr ?? 0);
|
||||
$bcr = $bcrPercentage / 100; // Convert to decimal
|
||||
$isBusiness = $this->isBusinessType();
|
||||
$unitPrice = $isBusiness ? 44300 : 16000;
|
||||
$calculatedAmount = $landArea * $bcr * $unitPrice;
|
||||
|
||||
return [
|
||||
'formula' => 'LUAS LAHAN × BCR (decimal) × HARGA SATUAN',
|
||||
'land_area' => $landArea,
|
||||
'bcr_percentage' => $bcrPercentage,
|
||||
'bcr_decimal' => $bcr,
|
||||
'business_type' => $isBusiness ? 'USAHA' : 'NON USAHA',
|
||||
'unit_price' => $unitPrice,
|
||||
'calculation' => "{$landArea} × {$bcr} × {$unitPrice}",
|
||||
'result' => $calculatedAmount,
|
||||
'building_function' => $this->building_function ?? $this->activities ?? 'N/A'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get old calculation amount from database
|
||||
*/
|
||||
public function getOldCalculationAmountAttribute(): float
|
||||
{
|
||||
$activeCalculation = $this->activeRetributionCalculation;
|
||||
|
||||
if ($activeCalculation && $activeCalculation->retributionCalculation) {
|
||||
return (float) $activeCalculation->retributionCalculation->retribution_amount;
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get calculation source info
|
||||
*/
|
||||
public function getCalculationSourceAttribute(): string
|
||||
{
|
||||
$manualCalculation = $this->calculateRetributionManually();
|
||||
$hasActiveCalculation = $this->hasActiveRetributionCalculation();
|
||||
|
||||
if ($manualCalculation > 0) {
|
||||
return $hasActiveCalculation ? 'NEW_FORMULA' : 'NEW_FORMULA_ONLY';
|
||||
} elseif ($hasActiveCalculation) {
|
||||
return 'OLD_DATABASE';
|
||||
}
|
||||
|
||||
return 'NONE';
|
||||
}
|
||||
}
|
||||
|
||||
23
app/Models/Tax.php
Normal file
23
app/Models/Tax.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Tax extends Model
|
||||
{
|
||||
protected $table = 'taxs';
|
||||
protected $fillable = [
|
||||
'tax_code',
|
||||
'tax_no',
|
||||
'npwpd',
|
||||
'wp_name',
|
||||
'business_name',
|
||||
'address',
|
||||
'start_validity',
|
||||
'end_validity',
|
||||
'tax_value',
|
||||
'subdistrict',
|
||||
'village',
|
||||
];
|
||||
}
|
||||
0
app/Models/User.php
Executable file → Normal file
0
app/Models/User.php
Executable file → Normal file
0
app/Providers/AppServiceProvider.php
Executable file → Normal file
0
app/Providers/AppServiceProvider.php
Executable file → Normal file
0
app/Providers/AuthServiceProvider.php
Executable file → Normal file
0
app/Providers/AuthServiceProvider.php
Executable file → Normal file
0
app/Providers/BroadcastServiceProvider.php
Executable file → Normal file
0
app/Providers/BroadcastServiceProvider.php
Executable file → Normal file
0
app/Providers/EventServiceProvider.php
Executable file → Normal file
0
app/Providers/EventServiceProvider.php
Executable file → Normal file
3
app/Providers/RouteServiceProvider.php
Executable file → Normal file
3
app/Providers/RouteServiceProvider.php
Executable file → Normal file
@@ -7,6 +7,7 @@ use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvi
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Models\Menu;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -17,7 +18,7 @@ class RouteServiceProvider extends ServiceProvider
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const HOME = '/home';
|
||||
public const HOME = '/dashboards/bigdata';
|
||||
|
||||
/**
|
||||
* Define your route model bindings, pattern filters, and other route configuration.
|
||||
|
||||
254
app/Services/RetributionCalculatorService.php
Normal file
254
app/Services/RetributionCalculatorService.php
Normal file
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\BuildingType;
|
||||
use App\Models\HeightIndex;
|
||||
use App\Models\RetributionConfig;
|
||||
use App\Models\RetributionCalculation;
|
||||
|
||||
class RetributionCalculatorService
|
||||
{
|
||||
/**
|
||||
* Calculate retribution for given parameters
|
||||
*/
|
||||
public function calculate(
|
||||
int $buildingTypeId,
|
||||
int $floorNumber,
|
||||
float $buildingArea,
|
||||
bool $saveResult = true,
|
||||
bool $excelCompatibleMode = false
|
||||
): array {
|
||||
// Get building type with indices
|
||||
$buildingType = BuildingType::with('indices')->findOrFail($buildingTypeId);
|
||||
|
||||
// Check if building type is free
|
||||
if ($buildingType->isFree()) {
|
||||
return $this->createFreeResult($buildingType, $floorNumber, $buildingArea, $saveResult);
|
||||
}
|
||||
|
||||
// Get height index
|
||||
$heightIndex = HeightIndex::getHeightIndexByFloor($floorNumber);
|
||||
|
||||
// Get configuration values
|
||||
$baseValue = RetributionConfig::getValue('BASE_VALUE', 70350);
|
||||
$infrastructureMultiplier = RetributionConfig::getValue('INFRASTRUCTURE_MULTIPLIER', 0.5);
|
||||
$heightMultiplier = RetributionConfig::getValue('HEIGHT_MULTIPLIER', 0.5);
|
||||
|
||||
// Get indices
|
||||
$indices = $buildingType->indices;
|
||||
if (!$indices) {
|
||||
throw new \Exception("Indices not found for building type: {$buildingType->name}");
|
||||
}
|
||||
|
||||
// Calculate using Excel formula
|
||||
$result = $this->executeCalculation(
|
||||
$buildingType,
|
||||
$indices,
|
||||
$heightIndex,
|
||||
$baseValue,
|
||||
$infrastructureMultiplier,
|
||||
$heightMultiplier,
|
||||
$floorNumber,
|
||||
$buildingArea,
|
||||
$excelCompatibleMode
|
||||
);
|
||||
|
||||
// Save result if requested
|
||||
if ($saveResult) {
|
||||
$calculation = RetributionCalculation::createCalculation(
|
||||
$buildingTypeId,
|
||||
$floorNumber,
|
||||
$buildingArea,
|
||||
$result['total_retribution'],
|
||||
$result['calculation_detail']
|
||||
);
|
||||
$result['calculation_id'] = $calculation->calculation_id;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the main calculation logic
|
||||
*/
|
||||
protected function executeCalculation(
|
||||
BuildingType $buildingType,
|
||||
$indices,
|
||||
float $heightIndex,
|
||||
float $baseValue,
|
||||
float $infrastructureMultiplier,
|
||||
float $heightMultiplier,
|
||||
int $floorNumber,
|
||||
float $buildingArea,
|
||||
bool $excelCompatibleMode = false
|
||||
): array {
|
||||
// Step 1: Calculate H5 coefficient (Excel formula: RUNDOWN(($E5*($F5+$G5+(0.5*H$3))),4))
|
||||
// H5 = coefficient * (ip_permanent + ip_complexity + (height_multiplier * height_index))
|
||||
$h5Raw = $indices->coefficient * (
|
||||
$indices->ip_permanent +
|
||||
$indices->ip_complexity +
|
||||
($heightMultiplier * $heightIndex)
|
||||
);
|
||||
|
||||
// Apply RUNDOWN (floor to 4 decimal places)
|
||||
$h5 = floor($h5Raw * 10000) / 10000;
|
||||
|
||||
// Step 2: Main calculation (Excel: 1*D5*(N5*base_value*H5*1))
|
||||
// Main = building_area * locality_index * base_value * h5
|
||||
$mainCalculation = $buildingArea * $indices->locality_index * $baseValue * $h5;
|
||||
|
||||
// Step 3: Infrastructure calculation (Excel: O3*(1*D5*(N5*base_value*H5*1)))
|
||||
// Additional = infrastructure_multiplier * main_calculation
|
||||
$infrastructureCalculation = $infrastructureMultiplier * $mainCalculation;
|
||||
|
||||
// Step 4: Total retribution (Main + Infrastructure)
|
||||
if ($excelCompatibleMode) {
|
||||
// Try to match Excel exactly - round intermediate calculations
|
||||
$mainCalculation = round($mainCalculation, 0);
|
||||
$infrastructureCalculation = round($infrastructureCalculation, 0);
|
||||
$totalRetribution = $mainCalculation + $infrastructureCalculation;
|
||||
} else {
|
||||
// Apply standard rounding to match Excel results more closely
|
||||
$totalRetribution = round($mainCalculation + $infrastructureCalculation, 0);
|
||||
}
|
||||
|
||||
return [
|
||||
'building_type' => [
|
||||
'id' => $buildingType->id,
|
||||
'code' => $buildingType->code,
|
||||
'name' => $buildingType->name,
|
||||
'is_free' => $buildingType->is_free
|
||||
],
|
||||
'input_parameters' => [
|
||||
'building_area' => $buildingArea,
|
||||
'floor_number' => $floorNumber,
|
||||
'height_index' => $heightIndex,
|
||||
'base_value' => $baseValue,
|
||||
'infrastructure_multiplier' => $infrastructureMultiplier,
|
||||
'height_multiplier' => $heightMultiplier
|
||||
],
|
||||
'indices' => [
|
||||
'coefficient' => $indices->coefficient,
|
||||
'ip_permanent' => $indices->ip_permanent,
|
||||
'ip_complexity' => $indices->ip_complexity,
|
||||
'locality_index' => $indices->locality_index,
|
||||
'infrastructure_factor' => $indices->infrastructure_factor
|
||||
],
|
||||
'calculation_steps' => [
|
||||
'h5_coefficient' => [
|
||||
'formula' => 'RUNDOWN((coefficient * (ip_permanent + ip_complexity + (height_multiplier * height_index))), 4)',
|
||||
'calculation' => "RUNDOWN(({$indices->coefficient} * ({$indices->ip_permanent} + {$indices->ip_complexity} + ({$heightMultiplier} * {$heightIndex}))), 4)",
|
||||
'raw_result' => $h5Raw,
|
||||
'result' => $h5
|
||||
],
|
||||
'main_calculation' => [
|
||||
'formula' => 'building_area * locality_index * base_value * h5',
|
||||
'calculation' => "{$buildingArea} * {$indices->locality_index} * {$baseValue} * {$h5}",
|
||||
'result' => $mainCalculation
|
||||
],
|
||||
'infrastructure_calculation' => [
|
||||
'formula' => 'infrastructure_multiplier * main_calculation',
|
||||
'calculation' => "{$infrastructureMultiplier} * {$mainCalculation}",
|
||||
'result' => $infrastructureCalculation
|
||||
],
|
||||
'total_calculation' => [
|
||||
'formula' => 'main_calculation + infrastructure_calculation',
|
||||
'calculation' => "{$mainCalculation} + {$infrastructureCalculation}",
|
||||
'result' => $totalRetribution
|
||||
]
|
||||
],
|
||||
'total_retribution' => $totalRetribution,
|
||||
'formatted_amount' => 'Rp ' . number_format($totalRetribution, 2, ',', '.'),
|
||||
'calculation_detail' => [
|
||||
'h5_raw' => $h5Raw,
|
||||
'h5' => $h5,
|
||||
'main' => $mainCalculation,
|
||||
'infrastructure' => $infrastructureCalculation,
|
||||
'total' => $totalRetribution
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create result for free building types
|
||||
*/
|
||||
protected function createFreeResult(
|
||||
BuildingType $buildingType,
|
||||
int $floorNumber,
|
||||
float $buildingArea,
|
||||
bool $saveResult
|
||||
): array {
|
||||
$result = [
|
||||
'building_type' => [
|
||||
'id' => $buildingType->id,
|
||||
'code' => $buildingType->code,
|
||||
'name' => $buildingType->name,
|
||||
'is_free' => true
|
||||
],
|
||||
'input_parameters' => [
|
||||
'building_area' => $buildingArea,
|
||||
'floor_number' => $floorNumber
|
||||
],
|
||||
'total_retribution' => 0.0,
|
||||
'formatted_amount' => 'Rp 0 (Gratis)',
|
||||
'calculation_detail' => [
|
||||
'reason' => 'Building type is free of charge',
|
||||
'total' => 0.0
|
||||
]
|
||||
];
|
||||
|
||||
if ($saveResult) {
|
||||
$calculation = RetributionCalculation::createCalculation(
|
||||
$buildingType->id,
|
||||
$floorNumber,
|
||||
$buildingArea,
|
||||
0.0,
|
||||
$result['calculation_detail']
|
||||
);
|
||||
$result['calculation_id'] = $calculation->calculation_id;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get calculation by ID
|
||||
*/
|
||||
public function getCalculationById(string $calculationId): ?RetributionCalculation
|
||||
{
|
||||
return RetributionCalculation::with('buildingType')
|
||||
->where('calculation_id', $calculationId)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available building types for calculation
|
||||
*/
|
||||
public function getAvailableBuildingTypes(): array
|
||||
{
|
||||
return BuildingType::with('indices')
|
||||
->active()
|
||||
->children() // Only child types can be used for calculation
|
||||
->get()
|
||||
->map(function ($type) {
|
||||
return [
|
||||
'id' => $type->id,
|
||||
'code' => $type->code,
|
||||
'name' => $type->name,
|
||||
'is_free' => $type->is_free,
|
||||
'has_indices' => $type->indices !== null,
|
||||
'coefficient' => $type->indices ? $type->indices->coefficient : null
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available floor numbers
|
||||
*/
|
||||
public function getAvailableFloors(): array
|
||||
{
|
||||
return HeightIndex::getAvailableFloors();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\BigdataResume;
|
||||
use App\Models\DataSetting;
|
||||
use App\Models\ImportDatasource;
|
||||
use App\Models\PbgTaskGoogleSheet;
|
||||
use App\Models\SpatialPlanning;
|
||||
use App\Models\RetributionCalculation;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Google_Client;
|
||||
use Google_Service_Sheets;
|
||||
use Log;
|
||||
use Google\Client as Google_Client;
|
||||
use Google\Service\Sheets as Google_Service_Sheets;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use App\Models\PbgTask;
|
||||
class ServiceGoogleSheet
|
||||
{
|
||||
protected $client;
|
||||
@@ -32,8 +39,8 @@ class ServiceGoogleSheet
|
||||
|
||||
public function run_service(){
|
||||
try{
|
||||
$this->sync_big_data();
|
||||
$this->sync_google_sheet_data();
|
||||
$this->sync_pbg_task_payments();
|
||||
}catch(Exception $e){
|
||||
throw $e;
|
||||
}
|
||||
@@ -131,7 +138,16 @@ class ServiceGoogleSheet
|
||||
}
|
||||
|
||||
// Count occurrences of each no_registrasi
|
||||
$registrasiCounts = array_count_values(array_column($mapUpsert, 'no_registrasi'));
|
||||
// Filter out null values before counting to avoid array_count_values error
|
||||
$registrationNumbers = array_filter(array_column($mapUpsert, 'no_registrasi'), function($value) {
|
||||
// Ensure only string and integer values are counted
|
||||
return $value !== null && $value !== '' && (is_string($value) || is_int($value));
|
||||
});
|
||||
|
||||
// Additional safety check: convert all values to strings
|
||||
$registrationNumbers = array_map('strval', $registrationNumbers);
|
||||
|
||||
$registrasiCounts = array_count_values($registrationNumbers);
|
||||
|
||||
// Filter duplicates (those appearing more than once)
|
||||
$duplicates = array_filter($registrasiCounts, function ($count) {
|
||||
@@ -142,8 +158,14 @@ class ServiceGoogleSheet
|
||||
Log::warning("Duplicate no_registrasi found", ['duplicates' => array_keys($duplicates)]);
|
||||
}
|
||||
|
||||
// Remove duplicates before upsert
|
||||
$mapUpsert = collect($mapUpsert)->unique('no_registrasi')->values()->all();
|
||||
// Remove duplicates before upsert - filter out entries with null no_registrasi
|
||||
$mapUpsert = collect($mapUpsert)
|
||||
->filter(function($item) {
|
||||
return !empty($item['no_registrasi']);
|
||||
})
|
||||
->unique('no_registrasi')
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$batchSize = 1000;
|
||||
$chunks = array_chunk($mapUpsert, $batchSize);
|
||||
@@ -196,9 +218,19 @@ class ServiceGoogleSheet
|
||||
}
|
||||
|
||||
foreach ($data_setting_result as $key => $value) {
|
||||
DataSetting::updateOrInsert(
|
||||
// Ensure value is not null before saving to database
|
||||
$processedValue = 0; // Default to 0 instead of null
|
||||
if ($value !== null && $value !== '') {
|
||||
if (strpos($key, '_COUNT') !== false) {
|
||||
$processedValue = $this->convertToInteger($value) ?? 0;
|
||||
} else {
|
||||
$processedValue = $this->convertToDecimal($value) ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
DataSetting::updateOrCreate(
|
||||
["key" => $key], // Find by key
|
||||
["value" => $value] // Update or insert value
|
||||
["value" => $processedValue] // Update or insert value
|
||||
);
|
||||
}
|
||||
|
||||
@@ -210,48 +242,618 @@ class ServiceGoogleSheet
|
||||
}
|
||||
}
|
||||
|
||||
public function sync_leader_data(){
|
||||
$import_datasource = ImportDatasource::create([
|
||||
'message' => 'Processing leader data',
|
||||
'status' => 'processing',
|
||||
'start_time' => now(),
|
||||
'failed_uuid' => null
|
||||
]);
|
||||
try {
|
||||
$sections = [
|
||||
'KEKURANGAN_POTENSI' => "DEVIASI TARGET DENGAN POTENSI TOTAL BERKAS",
|
||||
'TOTAL_POTENSI_BERKAS' => "•TOTAL BERKAS 2025",
|
||||
'BELUM_TERVERIFIKASI' => "•BERKAS AKTUAL BELUM TERVERIFIKASI (POTENSI):",
|
||||
'TERVERIFIKASI' => "•BERKAS AKTUAL TERVERIFIKASI DINAS TEKNIS 2025:",
|
||||
'NON_USAHA' => "•NON USAHA: HUNIAN, SOSBUD, KEAGAMAAN",
|
||||
'USAHA' => "•USAHA: USAHA, CAMPURAN, KOLEKTIF, PRASARANA",
|
||||
'PROSES_DINAS_TEKNIS' => "•TERPROSES DI DPUTR: belum selesai rekomtek'",
|
||||
'WAITING_KLIK_DPMPTSP' => "•TERPROSES DI PTSP: Pengiriman SKRD/ Validasi di PTSP",
|
||||
'REALISASI_TERBIT_PBG' => "•BERKAS YANG TERBIT PBG 2025:"
|
||||
];
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($sections as $key => $identifier) {
|
||||
$values = $this->get_values_from_section($identifier, [10, 11], 9);
|
||||
|
||||
if (!empty($values)) {
|
||||
$result[$key] = [
|
||||
'identifier' => $identifier,
|
||||
'total' => $values[0] ?? null, // index 0 untuk total/jumlah
|
||||
'nominal' => $values[1] ?? null // index 1 untuk nominal
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
BigdataResume::create([
|
||||
'import_datasource_id' => $import_datasource->id,
|
||||
'year' => date('Y'),
|
||||
'resume_type' => 'leader',
|
||||
// USAHA
|
||||
'business_count' => $this->convertToInteger($result['USAHA']['total'] ?? null) ?? 0,
|
||||
'business_sum' => $this->convertToDecimal($result['USAHA']['nominal'] ?? null) ?? 0,
|
||||
// NON USAHA
|
||||
'non_business_count' => $this->convertToInteger($result['NON_USAHA']['total'] ?? null) ?? 0,
|
||||
'non_business_sum' => $this->convertToDecimal($result['NON_USAHA']['nominal'] ?? null) ?? 0,
|
||||
// TERVERIFIKASI
|
||||
'verified_count' => $this->convertToInteger($result['TERVERIFIKASI']['total'] ?? null) ?? 0,
|
||||
'verified_sum' => $this->convertToDecimal($result['TERVERIFIKASI']['nominal'] ?? null) ?? 0,
|
||||
// BELUM TERVERIFIKASI
|
||||
'non_verified_count' => $this->convertToInteger($result['BELUM_TERVERIFIKASI']['total'] ?? null) ?? 0,
|
||||
'non_verified_sum' => $this->convertToDecimal($result['BELUM_TERVERIFIKASI']['nominal'] ?? null) ?? 0,
|
||||
// TOTAL POTENSI BERKAS
|
||||
'potention_count' => $this->convertToInteger($result['TOTAL_POTENSI_BERKAS']['total'] ?? null) ?? 0,
|
||||
'potention_sum' => $this->convertToDecimal($result['TOTAL_POTENSI_BERKAS']['nominal'] ?? null) ?? 0,
|
||||
// REALISASI TERBIT PBG
|
||||
'issuance_realization_pbg_count' => $this->convertToInteger($result['REALISASI_TERBIT_PBG']['total'] ?? null) ?? 0,
|
||||
'issuance_realization_pbg_sum' => $this->convertToDecimal($result['REALISASI_TERBIT_PBG']['nominal'] ?? null) ?? 0,
|
||||
// WAITING KLIK DPMPTSP
|
||||
'waiting_click_dpmptsp_count' => $this->convertToInteger($result['WAITING_KLIK_DPMPTSP']['total'] ?? null) ?? 0,
|
||||
'waiting_click_dpmptsp_sum' => $this->convertToDecimal($result['WAITING_KLIK_DPMPTSP']['nominal'] ?? null) ?? 0,
|
||||
// PROSES DINAS TEKNIS
|
||||
'process_in_technical_office_count' => $this->convertToInteger($result['PROSES_DINAS_TEKNIS']['total'] ?? null) ?? 0,
|
||||
'process_in_technical_office_sum' => $this->convertToDecimal($result['PROSES_DINAS_TEKNIS']['nominal'] ?? null) ?? 0,
|
||||
// TATA RUANG
|
||||
'spatial_count' => $this->getSpatialPlanningWithCalculationCount(),
|
||||
'spatial_sum' => $this->getSpatialPlanningCalculationSum(),
|
||||
'business_rab_count' => 0,
|
||||
'business_krk_count' => 0,
|
||||
'non_business_rab_count' => 0,
|
||||
'non_business_krk_count' => 0,
|
||||
'non_business_dlh_count' => 0,
|
||||
]);
|
||||
|
||||
// Save data settings
|
||||
$dataSettings = [
|
||||
'KEKURANGAN_POTENSI' => $result['KEKURANGAN_POTENSI']['nominal'] ?? null,
|
||||
'REALISASI_TERBIT_PBG_COUNT' => $result['REALISASI_TERBIT_PBG']['total'] ?? null,
|
||||
'REALISASI_TERBIT_PBG_SUM' => $result['REALISASI_TERBIT_PBG']['nominal'] ?? null,
|
||||
'MENUNGGU_KLIK_DPMPTSP_COUNT' => $result['WAITING_KLIK_DPMPTSP']['total'] ?? null,
|
||||
'MENUNGGU_KLIK_DPMPTSP_SUM' => $result['WAITING_KLIK_DPMPTSP']['nominal'] ?? null,
|
||||
'PROSES_DINAS_TEKNIS_COUNT' => $result['PROSES_DINAS_TEKNIS']['total'] ?? null,
|
||||
'PROSES_DINAS_TEKNIS_SUM' => $result['PROSES_DINAS_TEKNIS']['nominal'] ?? null,
|
||||
];
|
||||
|
||||
foreach ($dataSettings as $key => $value) {
|
||||
// Ensure value is not null before saving to database
|
||||
$processedValue = 0; // Default to 0 instead of null
|
||||
if ($value !== null && $value !== '') {
|
||||
// Try to convert to appropriate type based on key name
|
||||
if (strpos($key, '_COUNT') !== false) {
|
||||
$processedValue = $this->convertToInteger($value) ?? 0;
|
||||
} else {
|
||||
$processedValue = $this->convertToDecimal($value) ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
DataSetting::updateOrCreate(
|
||||
['key' => $key],
|
||||
['value' => $processedValue]
|
||||
);
|
||||
}
|
||||
|
||||
$import_datasource->update([
|
||||
'status' => 'success',
|
||||
'response_body' => json_encode($result),
|
||||
'message' => 'Leader data synced',
|
||||
'finish_time' => now()
|
||||
]);
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error syncing leader data", ['error' => $e->getMessage()]);
|
||||
$import_datasource->update([
|
||||
'status' => 'failed',
|
||||
'message' => 'Leader data sync failed',
|
||||
'finish_time' => now()
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function get_big_resume_data(){
|
||||
try {
|
||||
$sheet_big_data = $this->get_data_by_sheet();
|
||||
$data_setting_result = []; // Initialize result storage
|
||||
$sections = [
|
||||
'KEKURANGAN_POTENSI' => "DEVIASI TARGET DENGAN POTENSI TOTAL BERKAS",
|
||||
'TOTAL_POTENSI_BERKAS' => "•TOTAL BERKAS 2025",
|
||||
'BELUM_TERVERIFIKASI' => "•BERKAS AKTUAL BELUM TERVERIFIKASI (POTENSI):",
|
||||
'TERVERIFIKASI' => "•BERKAS AKTUAL TERVERIFIKASI DINAS TEKNIS 2025:",
|
||||
'NON_USAHA' => "•NON USAHA: HUNIAN, SOSBUD, KEAGAMAAN",
|
||||
'USAHA' => "•USAHA: USAHA, CAMPURAN, KOLEKTIF, PRASARANA",
|
||||
'PROSES_DINAS_TEKNIS' => "•TERPROSES DI DPUTR: belum selesai rekomtek'",
|
||||
'WAITING_KLIK_DPMPTSP' => "•TERPROSES DI PTSP: Pengiriman SKRD/ Validasi di PTSP",
|
||||
'REALISASI_TERBIT_PBG' => "•BERKAS YANG TERBIT PBG 2025:"
|
||||
];
|
||||
|
||||
$found_section = null; // Track which section is found
|
||||
$result = [];
|
||||
|
||||
foreach ($sheet_big_data as $row) {
|
||||
// Check for section headers
|
||||
if (in_array("•PROSES PENERBITAN:", $row)) {
|
||||
$found_section = "MENUNGGU_KLIK_DPMPTSP";
|
||||
} elseif (in_array("•BERKAS AKTUAL TERVERIFIKASI DINAS TEKNIS 2024:", $row)) {
|
||||
$found_section = "REALISASI_TERBIT_PBG";
|
||||
} elseif (in_array("•TERPROSES DI DPUTR: belum selesai rekomtek'", $row)) {
|
||||
$found_section = "PROSES_DINAS_TEKNIS";
|
||||
}
|
||||
foreach ($sections as $key => $identifier) {
|
||||
$values = $this->get_values_from_section($identifier, [10, 11], 9);
|
||||
|
||||
// If a section is found and we reach "Grand Total", save the corresponding values
|
||||
if ($found_section && isset($row[0]) && trim($row[0]) === "Grand Total") {
|
||||
if ($found_section === "MENUNGGU_KLIK_DPMPTSP") {
|
||||
$data_setting_result["MENUNGGU_KLIK_DPMPTSP_COUNT"] = $this->convertToInteger($row[2]) ?? null;
|
||||
$data_setting_result["MENUNGGU_KLIK_DPMPTSP_SUM"] = $this->convertToDecimal($row[3]) ?? null;
|
||||
} elseif ($found_section === "REALISASI_TERBIT_PBG") {
|
||||
$data_setting_result["REALISASI_TERBIT_PBG_COUNT"] = $this->convertToInteger($row[2]) ?? null;
|
||||
$data_setting_result["REALISASI_TERBIT_PBG_SUM"] = $this->convertToDecimal($row[4]) ?? null;
|
||||
} elseif ($found_section === "PROSES_DINAS_TEKNIS") {
|
||||
$data_setting_result["PROSES_DINAS_TEKNIS_COUNT"] = $this->convertToInteger($row[2]) ?? null;
|
||||
$data_setting_result["PROSES_DINAS_TEKNIS_SUM"] = $this->convertToDecimal($row[3]) ?? null;
|
||||
}
|
||||
|
||||
// Reset section tracking after capturing "Grand Total"
|
||||
$found_section = null;
|
||||
if (!empty($values)) {
|
||||
$result[$key] = [
|
||||
'identifier' => $identifier,
|
||||
'total' => $values[0] ?? null, // index 0 untuk total/jumlah
|
||||
'nominal' => $values[1] ?? null // index 1 untuk nominal
|
||||
];
|
||||
}
|
||||
}
|
||||
return $data_setting_result;
|
||||
|
||||
// Save data settings
|
||||
$dataSettings = [
|
||||
'KEKURANGAN_POTENSI' => $this->convertToDecimal($result['KEKURANGAN_POTENSI']['nominal']) ?? 0,
|
||||
'REALISASI_TERBIT_PBG_COUNT' => $this->convertToInteger($result['REALISASI_TERBIT_PBG']['total']) ?? 0,
|
||||
'REALISASI_TERBIT_PBG_SUM' => $this->convertToDecimal($result['REALISASI_TERBIT_PBG']['nominal']) ?? 0,
|
||||
'MENUNGGU_KLIK_DPMPTSP_COUNT' => $this->convertToInteger($result['WAITING_KLIK_DPMPTSP']['total']) ?? 0,
|
||||
'MENUNGGU_KLIK_DPMPTSP_SUM' => $this->convertToDecimal($result['WAITING_KLIK_DPMPTSP']['nominal']) ?? 0,
|
||||
'PROSES_DINAS_TEKNIS_COUNT' => $this->convertToInteger($result['PROSES_DINAS_TEKNIS']['total']) ?? 0,
|
||||
'PROSES_DINAS_TEKNIS_SUM' => $this->convertToDecimal($result['PROSES_DINAS_TEKNIS']['nominal']) ?? 0,
|
||||
'SPATIAL_PLANNING_COUNT' => $this->getSpatialPlanningWithCalculationCount(),
|
||||
'SPATIAL_PLANNING_SUM' => $this->getSpatialPlanningCalculationSum()
|
||||
];
|
||||
|
||||
foreach ($dataSettings as $key => $value) {
|
||||
// Ensure value is not null before saving to database
|
||||
$processedValue = 0; // Default to 0 instead of null
|
||||
if ($value !== null && $value !== '') {
|
||||
// Try to convert to appropriate type based on key name
|
||||
if (strpos($key, '_COUNT') !== false) {
|
||||
$processedValue = $this->convertToInteger($value) ?? 0;
|
||||
} else {
|
||||
$processedValue = $this->convertToDecimal($value) ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
DataSetting::updateOrCreate(
|
||||
['key' => $key],
|
||||
['value' => $processedValue]
|
||||
);
|
||||
}
|
||||
return $dataSettings;
|
||||
}catch(Exception $exception){
|
||||
Log::error("Error getting big resume data", ['error' => $exception->getMessage()]);
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
private function get_data_by_sheet($no_sheet = 1){
|
||||
/**
|
||||
* Get sheet data where the first row is treated as headers, and subsequent rows
|
||||
* are returned as associative arrays keyed by header names. Supports selecting
|
||||
* a contiguous column range plus additional specific columns.
|
||||
*
|
||||
* Example: get_sheet_data_with_headers_range('Data', 'A', 'AX', ['BX'])
|
||||
*
|
||||
* @param string $sheet_name
|
||||
* @param string $start_column_letter Inclusive start column letter (e.g., 'A')
|
||||
* @param string $end_column_letter Inclusive end column letter (e.g., 'AX')
|
||||
* @param array $extra_column_letters Additional discrete column letters (e.g., ['BX'])
|
||||
* @return array{headers: array<int,string>, data: array<int,array<string,?string>>, selected_columns: array<int,int>}
|
||||
*/
|
||||
public function get_sheet_data_with_headers_range(string $sheet_name, string $start_column_letter, string $end_column_letter, array $extra_column_letters = [])
|
||||
{
|
||||
try {
|
||||
$sheet_data = $this->get_data_by_sheet_name($sheet_name);
|
||||
|
||||
if (empty($sheet_data)) {
|
||||
Log::warning("No data found in sheet", ['sheet_name' => $sheet_name]);
|
||||
return [
|
||||
'headers' => [],
|
||||
'data' => [],
|
||||
'selected_columns' => []
|
||||
];
|
||||
}
|
||||
|
||||
// Build selected column indices: range A..AX and extras like BX
|
||||
$selected_indices = $this->expandColumnRangeToIndices($start_column_letter, $end_column_letter);
|
||||
foreach ($extra_column_letters as $letter) {
|
||||
$selected_indices[] = $this->columnLetterToIndex($letter);
|
||||
}
|
||||
// Ensure unique and sorted
|
||||
$selected_indices = array_values(array_unique($selected_indices));
|
||||
sort($selected_indices);
|
||||
|
||||
$result = [
|
||||
'headers' => [],
|
||||
'data' => [],
|
||||
'selected_columns' => $selected_indices
|
||||
];
|
||||
|
||||
foreach ($sheet_data as $row_index => $row) {
|
||||
if (!is_array($row)) continue;
|
||||
|
||||
if ($row_index === 0) {
|
||||
// First row contains headers (by selected columns)
|
||||
foreach ($selected_indices as $col_index) {
|
||||
$raw = isset($row[$col_index]) ? trim((string) $row[$col_index]) : '';
|
||||
// Fallback to column letter if empty
|
||||
$header = $raw !== '' ? $raw : $this->indexToColumnLetter($col_index);
|
||||
$result['headers'][$col_index] = $this->normalizeHeader($header);
|
||||
}
|
||||
} else {
|
||||
$row_assoc = [];
|
||||
$has_data = false;
|
||||
foreach ($selected_indices as $col_index) {
|
||||
$header = $result['headers'][$col_index] ?? $this->normalizeHeader($this->indexToColumnLetter($col_index));
|
||||
$value = isset($row[$col_index]) ? trim((string) $row[$col_index]) : '';
|
||||
$row_assoc[$header] = ($value === '') ? null : $value;
|
||||
if ($value !== '') {
|
||||
$has_data = true;
|
||||
}
|
||||
}
|
||||
if ($has_data) {
|
||||
$result['data'][] = $row_assoc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error getting sheet data with headers", [
|
||||
'sheet_name' => $sheet_name,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a column letter (e.g., 'A', 'Z', 'AA', 'AX', 'BX') to a zero-based index (A=0)
|
||||
*/
|
||||
private function columnLetterToIndex(string $letter): int
|
||||
{
|
||||
$letter = strtoupper(trim($letter));
|
||||
$length = strlen($letter);
|
||||
$index = 0;
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$index = $index * 26 + (ord($letter[$i]) - ord('A') + 1);
|
||||
}
|
||||
return $index - 1; // zero-based
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert zero-based column index to column letter (0='A')
|
||||
*/
|
||||
private function indexToColumnLetter(int $index): string
|
||||
{
|
||||
$index += 1; // make 1-based for calculation
|
||||
$letters = '';
|
||||
while ($index > 0) {
|
||||
$mod = ($index - 1) % 26;
|
||||
$letters = chr($mod + ord('A')) . $letters;
|
||||
$index = intdiv($index - 1, 26);
|
||||
}
|
||||
return $letters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a column range like 'A'..'AX' to zero-based indices array
|
||||
*/
|
||||
private function expandColumnRangeToIndices(string $start_letter, string $end_letter): array
|
||||
{
|
||||
$start = $this->columnLetterToIndex($start_letter);
|
||||
$end = $this->columnLetterToIndex($end_letter);
|
||||
if ($start > $end) {
|
||||
[$start, $end] = [$end, $start];
|
||||
}
|
||||
return range($start, $end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize header: trim, lowercase, replace spaces with underscore, remove non-alnum/underscore
|
||||
*/
|
||||
private function normalizeHeader(string $header): string
|
||||
{
|
||||
$header = trim($header);
|
||||
$header = strtolower($header);
|
||||
$header = preg_replace('/\s+/', '_', $header);
|
||||
$header = preg_replace('/[^a-z0-9_]/', '', $header);
|
||||
return $header;
|
||||
}
|
||||
|
||||
public function sync_pbg_task_payments(){
|
||||
try {
|
||||
$sheetName = 'Data';
|
||||
$startLetter = 'A';
|
||||
$endLetter = 'AX';
|
||||
$extraLetters = ['BF'];
|
||||
|
||||
// Fetch header row only (row 1) across A..BF and build header/selection
|
||||
$headerRange = sprintf('%s!%s1:%s1', $sheetName, $startLetter, 'BF');
|
||||
$headerResponse = $this->service->spreadsheets_values->get($this->spreadsheetID, $headerRange);
|
||||
$headerRow = $headerResponse->getValues()[0] ?? [];
|
||||
if (empty($headerRow)) {
|
||||
Log::warning("No header row found in sheet", ['sheet' => $sheetName]);
|
||||
return ['success' => false, 'message' => 'No header row found'];
|
||||
}
|
||||
|
||||
// Selected indices: A..AX plus BF
|
||||
$selected_indices = $this->expandColumnRangeToIndices($startLetter, $endLetter);
|
||||
foreach ($extraLetters as $letter) {
|
||||
$selected_indices[] = $this->columnLetterToIndex($letter);
|
||||
}
|
||||
$selected_indices = array_values(array_unique($selected_indices));
|
||||
sort($selected_indices);
|
||||
|
||||
// Build normalized headers map (index -> header)
|
||||
$headers = [];
|
||||
foreach ($selected_indices as $colIdx) {
|
||||
$raw = isset($headerRow[$colIdx]) ? trim((string) $headerRow[$colIdx]) : '';
|
||||
$header = $raw !== '' ? $raw : $this->indexToColumnLetter($colIdx);
|
||||
$headers[$colIdx] = $this->normalizeHeader($header);
|
||||
}
|
||||
|
||||
// Log environment and header diagnostics
|
||||
Log::info('sync_pbg_task_payments: diagnostics', [
|
||||
'spreadsheet_id' => $this->spreadsheetID,
|
||||
'sheet' => $sheetName,
|
||||
'selected_indices_count' => count($selected_indices)
|
||||
]);
|
||||
|
||||
// Validate that expected headers exist after normalization before truncating table
|
||||
$expectedHeaders = [
|
||||
'no','jenis_konsultasi','no_registrasi','nama_pemilik','lokasi_bg','fungsi_bg','nama_bangunan',
|
||||
'tgl_permohonan','status_verifikasi','status_permohonan','alamat_pemilik','no_hp','email',
|
||||
'tanggal_catatan','catatan_kekurangan_dokumen','gambar','krkkkpr','no_krk','lh','ska','keterangan',
|
||||
'helpdesk','pj','operator_pbg','kepemilikan','potensi_taru','validasi_dinas','kategori_retribusi',
|
||||
'no_urut_ba_tpt_20250001','tanggal_ba_tpt','no_urut_ba_tpa','tanggal_ba_tpa','no_urut_skrd_20250001',
|
||||
'tanggal_skrd','ptsp','selesai_terbit','tanggal_pembayaran_yyyymmdd','format_sts','tahun_terbit',
|
||||
'tahun_berjalan','kelurahan','kecamatan','lb','tb','jlb','unit','usulan_retribusi',
|
||||
'nilai_retribusi_keseluruhan_simbg','nilai_retribusi_keseluruhan_pad','denda','usaha__non_usaha'
|
||||
];
|
||||
|
||||
$normalizedHeaderValues = array_values($headers);
|
||||
$overlap = array_intersect($expectedHeaders, $normalizedHeaderValues);
|
||||
|
||||
if (count($overlap) < 10) { // too few matching headers, likely wrong sheet or headers changed
|
||||
Log::error('sync_pbg_task_payments: header mismatch detected', [
|
||||
'expected_sample' => array_slice($expectedHeaders, 0, 15),
|
||||
'found_sample' => array_slice($normalizedHeaderValues, 0, 30),
|
||||
'match_count' => count($overlap)
|
||||
]);
|
||||
return ['success' => false, 'message' => 'Header mismatch - aborting to prevent null inserts'];
|
||||
}
|
||||
|
||||
// Truncate table and restart identity (only after header validation)
|
||||
Schema::disableForeignKeyConstraints();
|
||||
DB::table('pbg_task_payments')->truncate();
|
||||
Schema::enableForeignKeyConstraints();
|
||||
|
||||
// Map header -> db column
|
||||
$map = [
|
||||
'no' => 'row_no',
|
||||
'jenis_konsultasi' => 'consultation_type',
|
||||
'no_registrasi' => 'source_registration_number',
|
||||
'nama_pemilik' => 'owner_name',
|
||||
'lokasi_bg' => 'building_location',
|
||||
'fungsi_bg' => 'building_function',
|
||||
'nama_bangunan' => 'building_name',
|
||||
'tgl_permohonan' => 'application_date_raw',
|
||||
'status_verifikasi' => 'verification_status',
|
||||
'status_permohonan' => 'application_status',
|
||||
'alamat_pemilik' => 'owner_address',
|
||||
'no_hp' => 'owner_phone',
|
||||
'email' => 'owner_email',
|
||||
'tanggal_catatan' => 'note_date_raw',
|
||||
'catatan_kekurangan_dokumen' => 'document_shortage_note',
|
||||
'gambar' => 'image_url',
|
||||
'krkkkpr' => 'krk_kkpr',
|
||||
'no_krk' => 'krk_number',
|
||||
'lh' => 'lh',
|
||||
'ska' => 'ska',
|
||||
'keterangan' => 'remarks',
|
||||
'helpdesk' => 'helpdesk',
|
||||
'pj' => 'person_in_charge',
|
||||
'operator_pbg' => 'pbg_operator',
|
||||
'kepemilikan' => 'ownership',
|
||||
'potensi_taru' => 'taru_potential',
|
||||
'validasi_dinas' => 'agency_validation',
|
||||
'kategori_retribusi' => 'retribution_category',
|
||||
'no_urut_ba_tpt_20250001' => 'ba_tpt_number',
|
||||
'tanggal_ba_tpt' => 'ba_tpt_date_raw',
|
||||
'no_urut_ba_tpa' => 'ba_tpa_number',
|
||||
'tanggal_ba_tpa' => 'ba_tpa_date_raw',
|
||||
'no_urut_skrd_20250001' => 'skrd_number',
|
||||
'tanggal_skrd' => 'skrd_date_raw',
|
||||
'ptsp' => 'ptsp_status',
|
||||
'selesai_terbit' => 'issued_status',
|
||||
'tanggal_pembayaran_yyyymmdd' => 'payment_date_raw',
|
||||
'format_sts' => 'sts_format',
|
||||
'tahun_terbit' => 'issuance_year',
|
||||
'tahun_berjalan' => 'current_year',
|
||||
'kelurahan' => 'village',
|
||||
'kecamatan' => 'district',
|
||||
'lb' => 'building_area',
|
||||
'tb' => 'building_height',
|
||||
'jlb' => 'floor_count',
|
||||
'unit' => 'unit_count',
|
||||
'usulan_retribusi' => 'proposed_retribution',
|
||||
'nilai_retribusi_keseluruhan_simbg' => 'retribution_total_simbg',
|
||||
'nilai_retribusi_keseluruhan_pad' => 'retribution_total_pad',
|
||||
'denda' => 'penalty_amount',
|
||||
'usaha__non_usaha' => 'business_category',
|
||||
];
|
||||
|
||||
// We'll build registration map lazily per chunk to limit memory
|
||||
$regToTask = [];
|
||||
|
||||
// Build and insert in small batches to avoid high memory usage
|
||||
$batch = [];
|
||||
$batchSize = 500;
|
||||
$inserted = 0;
|
||||
// Stream rows in chunks from API to avoid loading full sheet
|
||||
$rowStart = 2; // data starts from row 2
|
||||
$chunkRowSize = 800; // number of rows per chunk
|
||||
$inserted = 0;
|
||||
while (true) {
|
||||
$rowEnd = $rowStart + $chunkRowSize - 1;
|
||||
$range = sprintf('%s!%s%d:%s%d', $sheetName, $startLetter, $rowStart, 'BF', $rowEnd);
|
||||
$resp = $this->service->spreadsheets_values->get($this->spreadsheetID, $range);
|
||||
$values = $resp->getValues() ?? [];
|
||||
if (empty($values)) {
|
||||
break; // no more rows
|
||||
}
|
||||
|
||||
// Preload registration map for this chunk
|
||||
$chunkRegs = [];
|
||||
foreach ($values as $row) {
|
||||
foreach ($selected_indices as $colIdx) {
|
||||
// find normalized header for this index
|
||||
$h = $headers[$colIdx] ?? null;
|
||||
if ($h === 'no_registrasi') {
|
||||
$val = isset($row[$colIdx]) ? trim((string) $row[$colIdx]) : '';
|
||||
if ($val !== '') { $chunkRegs[$val] = true; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!empty($chunkRegs)) {
|
||||
$keys = array_keys($chunkRegs);
|
||||
$tasks = PbgTask::whereIn('registration_number', $keys)->get(['id','uuid','registration_number']);
|
||||
foreach ($tasks as $task) {
|
||||
$regToTask[trim($task->registration_number)] = ['id' => $task->id, 'uuid' => $task->uuid];
|
||||
}
|
||||
}
|
||||
|
||||
// Build and insert this chunk
|
||||
$batch = [];
|
||||
foreach ($values as $rowIndex => $row) {
|
||||
$record = [
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
|
||||
// Map row values by headers
|
||||
$rowByHeader = [];
|
||||
foreach ($selected_indices as $colIdx) {
|
||||
$h = $headers[$colIdx] ?? null;
|
||||
if ($h === null) continue;
|
||||
$rowByHeader[$h] = isset($row[$colIdx]) ? trim((string) $row[$colIdx]) : null;
|
||||
if ($rowByHeader[$h] === '') $rowByHeader[$h] = null;
|
||||
}
|
||||
|
||||
// Log first non-empty row mapping for diagnostics
|
||||
if ($rowIndex === 0) {
|
||||
$nonEmptySample = [];
|
||||
foreach ($rowByHeader as $k => $v) {
|
||||
if ($v !== null && count($nonEmptySample) < 10) { $nonEmptySample[$k] = $v; }
|
||||
}
|
||||
Log::info('sync_pbg_task_payments: first row sample after normalization', [
|
||||
'sample' => $nonEmptySample
|
||||
]);
|
||||
}
|
||||
|
||||
// Skip if this row looks like a header row
|
||||
$headerCheckKeys = ['no','jenis_konsultasi','no_registrasi'];
|
||||
$headerMatches = 0;
|
||||
foreach ($headerCheckKeys as $hk) {
|
||||
if (!array_key_exists($hk, $rowByHeader)) { continue; }
|
||||
$val = $rowByHeader[$hk];
|
||||
if ($val === null) { continue; }
|
||||
if ($this->normalizeHeader($val) === $hk) {
|
||||
$headerMatches++;
|
||||
}
|
||||
}
|
||||
if ($headerMatches >= 2) {
|
||||
continue; // looks like a repeated header row, skip
|
||||
}
|
||||
|
||||
// Skip if the entire row is empty (no values)
|
||||
$hasAnyData = false;
|
||||
foreach ($rowByHeader as $v) {
|
||||
if ($v !== null && $v !== '') { $hasAnyData = true; break; }
|
||||
}
|
||||
if (!$hasAnyData) { continue; }
|
||||
|
||||
foreach ($map as $header => $column) {
|
||||
$value = $rowByHeader[$header] ?? null;
|
||||
|
||||
switch ($column) {
|
||||
case 'row_no':
|
||||
case 'floor_count':
|
||||
case 'unit_count':
|
||||
case 'issuance_year':
|
||||
case 'current_year':
|
||||
$record[$column] = ($value === null || $value === '') ? null : (int) $value;
|
||||
break;
|
||||
case 'application_date_raw':
|
||||
case 'note_date_raw':
|
||||
case 'ba_tpt_date_raw':
|
||||
case 'ba_tpa_date_raw':
|
||||
case 'skrd_date_raw':
|
||||
case 'payment_date_raw':
|
||||
$record[$column] = $this->convertToDate($value);
|
||||
break;
|
||||
case 'building_area':
|
||||
case 'building_height':
|
||||
case 'proposed_retribution':
|
||||
case 'retribution_total_simbg':
|
||||
case 'retribution_total_pad':
|
||||
case 'penalty_amount':
|
||||
$record[$column] = $this->convertToDecimal($value);
|
||||
break;
|
||||
default:
|
||||
if (is_string($value)) { $value = trim($value); }
|
||||
$record[$column] = ($value === '' ? null : $value);
|
||||
}
|
||||
}
|
||||
|
||||
// Final trim pass
|
||||
foreach ($record as $k => $v) {
|
||||
if (is_string($v)) {
|
||||
$t = trim($v);
|
||||
$record[$k] = ($t === '') ? null : $t;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve relation
|
||||
$sourceReg = $rowByHeader['no_registrasi'] ?? null;
|
||||
if (is_string($sourceReg)) { $sourceReg = trim($sourceReg); }
|
||||
if (!empty($sourceReg) && isset($regToTask[$sourceReg])) {
|
||||
$record['pbg_task_id'] = $regToTask[$sourceReg]['id'];
|
||||
$record['pbg_task_uid'] = $regToTask[$sourceReg]['uuid'];
|
||||
} else {
|
||||
$record['pbg_task_id'] = null;
|
||||
$record['pbg_task_uid'] = null;
|
||||
}
|
||||
|
||||
$batch[] = $record;
|
||||
}
|
||||
|
||||
if (!empty($batch)) {
|
||||
\App\Models\PbgTaskPayment::insert($batch);
|
||||
$inserted += count($batch);
|
||||
}
|
||||
|
||||
// next chunk
|
||||
$rowStart = $rowEnd + 1;
|
||||
if (function_exists('gc_collect_cycles')) { gc_collect_cycles(); }
|
||||
}
|
||||
|
||||
if (!empty($batch)) {
|
||||
\App\Models\PbgTaskPayment::insert($batch);
|
||||
$inserted += count($batch);
|
||||
}
|
||||
|
||||
Log::info('PBG Task Payments reloaded from sheet', ['inserted' => $inserted]);
|
||||
|
||||
return ['success' => true, 'inserted' => $inserted];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error syncing PBG task payments", ['error' => $e->getMessage()]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function get_data_by_sheet($no_sheet = 8){
|
||||
$spreadsheet = $this->service->spreadsheets->get($this->spreadsheetID);
|
||||
$sheets = $spreadsheet->getSheets();
|
||||
$sheetTitle = $sheets[$no_sheet]->getProperties()->getTitle();
|
||||
@@ -261,9 +863,110 @@ class ServiceGoogleSheet
|
||||
return!empty($values)? $values : [];
|
||||
}
|
||||
|
||||
private function get_data_by_sheet_name($sheet_name){
|
||||
try {
|
||||
$spreadsheet = $this->service->spreadsheets->get($this->spreadsheetID);
|
||||
$sheets = $spreadsheet->getSheets();
|
||||
|
||||
// Find sheet by name
|
||||
$targetSheet = null;
|
||||
foreach ($sheets as $sheet) {
|
||||
if ($sheet->getProperties()->getTitle() === $sheet_name) {
|
||||
$targetSheet = $sheet;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$targetSheet) {
|
||||
Log::warning("Sheet not found", ['sheet_name' => $sheet_name]);
|
||||
return [];
|
||||
}
|
||||
|
||||
$range = "{$sheet_name}";
|
||||
$response = $this->service->spreadsheets_values->get($this->spreadsheetID, $range);
|
||||
$values = $response->getValues();
|
||||
|
||||
Log::info("Sheet data retrieved", [
|
||||
'sheet_name' => $sheet_name,
|
||||
'total_rows' => count($values ?? [])
|
||||
]);
|
||||
|
||||
return !empty($values) ? $values : [];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error getting data by sheet name", [
|
||||
'sheet_name' => $sheet_name,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific values from a row that contains a specific text/section identifier
|
||||
* @param string $section_identifier Text to search for in the row
|
||||
* @param array $column_indices Array of column indices to extract values from
|
||||
* @param int $no_sheet Sheet number (0-based)
|
||||
* @return array Array of values from specified columns, or empty array if section not found
|
||||
*/
|
||||
private function get_values_from_section(string $section_identifier, array $column_indices = [], int $no_sheet = 8) {
|
||||
try {
|
||||
$sheet_data = $this->get_data_by_sheet($no_sheet);
|
||||
|
||||
if (empty($sheet_data)) {
|
||||
Log::warning("No data found in sheet", ['sheet' => $no_sheet]);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Search for the row containing the section identifier
|
||||
$target_row = null;
|
||||
foreach ($sheet_data as $row_index => $row) {
|
||||
if (is_array($row)) {
|
||||
foreach ($row as $cell) {
|
||||
if (is_string($cell) && strpos($cell, $section_identifier) !== false) {
|
||||
$target_row = $row;
|
||||
break 2; // Break out of both loops
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($target_row === null) {
|
||||
Log::warning("Section not found", ['section_identifier' => $section_identifier]);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Extract values from specified column indices
|
||||
$extracted_values = [];
|
||||
foreach ($column_indices as $col_index) {
|
||||
if (isset($target_row[$col_index])) {
|
||||
$value = trim($target_row[$col_index]);
|
||||
$extracted_values[] = $value !== '' ? $value : null;
|
||||
} else {
|
||||
$extracted_values[] = null;
|
||||
}
|
||||
}
|
||||
|
||||
Log::info("Values extracted from section", [
|
||||
'section_identifier' => $section_identifier,
|
||||
'column_indices' => $column_indices,
|
||||
'extracted_values' => $extracted_values
|
||||
]);
|
||||
|
||||
return $extracted_values;
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error getting values from section", [
|
||||
'error' => $e->getMessage(),
|
||||
'section_identifier' => $section_identifier,
|
||||
'sheet' => $no_sheet
|
||||
]);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function convertToInteger($value) {
|
||||
// Check if the value is an empty string, and return null if true
|
||||
if (trim($value) === "") {
|
||||
// Check if the value is null or empty string, and return null if true
|
||||
if ($value === null || trim($value) === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -300,6 +1003,54 @@ class ServiceGoogleSheet
|
||||
return is_numeric($value) ? (float) number_format((float) $value, 2, '.', '') : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of spatial plannings that can be calculated with new formula
|
||||
*/
|
||||
public function getSpatialPlanningWithCalculationCount(): int
|
||||
{
|
||||
try {
|
||||
// Count spatial plannings that have valid data and are not yet issued (is_terbit = false)
|
||||
return SpatialPlanning::where('land_area', '>', 0)
|
||||
->where('site_bcr', '>', 0)
|
||||
->where('is_terbit', false)
|
||||
->count();
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error getting spatial planning with calculation count", ['error' => $e->getMessage()]);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total sum of retribution amounts using new calculation formula
|
||||
*/
|
||||
public function getSpatialPlanningCalculationSum(): float
|
||||
{
|
||||
try {
|
||||
// Get spatial plannings that are not yet issued (is_terbit = false) and have valid data
|
||||
$spatialPlannings = SpatialPlanning::where('land_area', '>', 0)
|
||||
->where('site_bcr', '>', 0)
|
||||
->where('is_terbit', false)
|
||||
->get();
|
||||
|
||||
$totalSum = 0;
|
||||
foreach ($spatialPlannings as $spatialPlanning) {
|
||||
// Use new calculation formula: LUAS LAHAN × BCR × HARGA SATUAN
|
||||
$totalSum += $spatialPlanning->calculated_retribution;
|
||||
}
|
||||
|
||||
Log::info("Spatial Planning Calculation Sum (is_terbit = false only)", [
|
||||
'total_records' => $spatialPlannings->count(),
|
||||
'total_sum' => $totalSum,
|
||||
'filtered_by' => 'is_terbit = false'
|
||||
]);
|
||||
|
||||
return (float) $totalSum;
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error getting spatial planning calculation sum", ['error' => $e->getMessage()]);
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
private function convertToDate($dateString)
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -106,7 +106,7 @@ class ServicePbgTask
|
||||
};
|
||||
|
||||
do {
|
||||
$url = "{$this->simbg_host}/api/pbg/v1/list/?page={$currentPage}&size={$this->fetch_per_page}&sort=ASC";
|
||||
$url = "{$this->simbg_host}/api/pbg/v1/list/?page={$currentPage}&size={$this->fetch_per_page}&sort=ASC&date&search&status&slf_status&type=task&sort_by=created_at&application_type=1&start_date&end_date";
|
||||
|
||||
$fetch_data = $fetchData($url);
|
||||
if (!$fetch_data) {
|
||||
@@ -123,6 +123,8 @@ class ServicePbgTask
|
||||
$data = $response['data'];
|
||||
$totalPage = isset($response['total_page']) ? (int) $response['total_page'] : 1;
|
||||
|
||||
Log::info("Total data scraping {$totalPage}");
|
||||
|
||||
$saved_data = [];
|
||||
foreach ($data as $item) {
|
||||
$saved_data[] = [
|
||||
|
||||
@@ -421,8 +421,7 @@ class ServiceSIMBG
|
||||
}
|
||||
}
|
||||
|
||||
BigdataResume::generateResumeData($importDatasource->id, "all", $data_setting_result);
|
||||
BigdataResume::generateResumeData($importDatasource->id, now()->year, $data_setting_result);
|
||||
BigdataResume::generateResumeData($importDatasource->id, date('Y'), "simbg");
|
||||
|
||||
// Final update after processing all pages
|
||||
$importDatasource->update([
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\GlobalSetting;
|
||||
use App\Models\PbgStatus;
|
||||
use App\Models\PbgTask;
|
||||
use App\Models\PbgTaskDetail;
|
||||
use App\Models\PbgTaskDetailDataList;
|
||||
use App\Models\PbgTaskIndexIntegrations;
|
||||
use App\Models\PbgTaskPrasarana;
|
||||
use App\Models\PbgTaskRetributions;
|
||||
@@ -35,43 +38,261 @@ class ServiceTabPbgTask
|
||||
$this->user_refresh_token = $auth_data['refresh'];
|
||||
}
|
||||
|
||||
public function run_service($retry_uuid = null)
|
||||
public function run_service($retry_uuid = null, $chunk_size = 50)
|
||||
{
|
||||
try {
|
||||
$pbg_tasks = PbgTask::orderBy('id')->get();
|
||||
$start = false;
|
||||
$query = PbgTask::orderBy('id');
|
||||
|
||||
foreach ($pbg_tasks as $pbg_task) {
|
||||
// If retry_uuid is provided, start from that UUID
|
||||
if ($retry_uuid) {
|
||||
if($pbg_task->uuid === $retry_uuid){
|
||||
$start = true;
|
||||
$retryTask = PbgTask::where('uuid', $retry_uuid)->first();
|
||||
if ($retryTask) {
|
||||
$query->where('id', '>=', $retryTask->id);
|
||||
Log::info("Resuming sync from UUID: {$retry_uuid} (ID: {$retryTask->id})");
|
||||
}
|
||||
}
|
||||
|
||||
if(!$start){
|
||||
$totalTasks = $query->count();
|
||||
$processedCount = 0;
|
||||
|
||||
Log::info("Starting sync for {$totalTasks} PBG Tasks with chunk size: {$chunk_size}");
|
||||
|
||||
// Process in chunks to reduce memory usage
|
||||
$query->chunk($chunk_size, function ($pbg_tasks) use (&$processedCount, $totalTasks) {
|
||||
$chunkStartTime = now();
|
||||
|
||||
foreach ($pbg_tasks as $pbg_task) {
|
||||
try {
|
||||
$this->current_uuid = $pbg_task->uuid;
|
||||
$taskStartTime = now();
|
||||
|
||||
// Process all endpoints for this task
|
||||
$this->processTaskEndpoints($pbg_task->uuid);
|
||||
|
||||
$processedCount++;
|
||||
$taskTime = now()->diffInSeconds($taskStartTime);
|
||||
|
||||
// Log progress every 10 tasks
|
||||
if ($processedCount % 10 === 0) {
|
||||
$progress = round(($processedCount / $totalTasks) * 100, 2);
|
||||
Log::info("Progress: {$processedCount}/{$totalTasks} ({$progress}%) - Last task took {$taskTime}s");
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Failed on UUID: {$this->current_uuid}, Error: " . $e->getMessage());
|
||||
|
||||
// Check if this is a critical error that should stop the process
|
||||
if ($this->isCriticalError($e)) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// For non-critical errors, log and continue
|
||||
Log::warning("Skipping UUID {$this->current_uuid} due to non-critical error");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
try{
|
||||
$this->current_uuid = $pbg_task->uuid;
|
||||
$this->scraping_task_assignments($pbg_task->uuid);
|
||||
$this->scraping_task_retributions($pbg_task->uuid);
|
||||
$this->scraping_task_integrations($pbg_task->uuid);
|
||||
|
||||
$chunkTime = now()->diffInSeconds($chunkStartTime);
|
||||
Log::info("Processed chunk of {$pbg_tasks->count()} tasks in {$chunkTime} seconds");
|
||||
|
||||
// Small delay between chunks to prevent API rate limiting
|
||||
if ($pbg_tasks->count() === $chunk_size) {
|
||||
sleep(1);
|
||||
}
|
||||
});
|
||||
|
||||
Log::info("Successfully completed sync for {$processedCount} PBG Tasks");
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Failed on UUID: {$this->current_uuid}, Error: " . $e->getMessage());
|
||||
Log::error("Failed to synchronize: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Failed to syncronize: " . $e->getMessage());
|
||||
throw $e;
|
||||
|
||||
/**
|
||||
* Process all endpoints for a single task
|
||||
*/
|
||||
private function processTaskEndpoints(string $uuid): void
|
||||
{
|
||||
$this->scraping_task_details($uuid);
|
||||
$this->scraping_pbg_data_list($uuid);
|
||||
// $this->scraping_task_assignments($uuid);
|
||||
$this->scraping_task_retributions($uuid);
|
||||
$this->scraping_task_integrations($uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an error is critical and should stop the process
|
||||
*/
|
||||
private function isCriticalError(\Exception $e): bool
|
||||
{
|
||||
$message = $e->getMessage();
|
||||
|
||||
// Critical authentication errors
|
||||
if (strpos($message, 'Token refresh and login failed') !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Critical system errors
|
||||
if (strpos($message, 'Connection refused') !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Database connection errors
|
||||
if (strpos($message, 'database') !== false && strpos($message, 'connection') !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getFailedUUID(){
|
||||
return $this->current_uuid;
|
||||
}
|
||||
|
||||
private function scraping_task_assignments($uuid)
|
||||
public function scraping_task_details($uuid)
|
||||
{
|
||||
$url = "{$this->simbg_host}/api/pbg/v1/detail/{$uuid}/";
|
||||
$options = [
|
||||
'headers' => [
|
||||
'Authorization' => "Bearer {$this->user_token}",
|
||||
'Content-Type' => 'application/json'
|
||||
]
|
||||
];
|
||||
|
||||
$maxRetries = 3;
|
||||
$initialDelay = 1;
|
||||
$retriedAfter401 = false;
|
||||
|
||||
for ($retryCount = 0; $retryCount < $maxRetries; $retryCount++) {
|
||||
try {
|
||||
$response = $this->client->get($url, $options);
|
||||
$responseData = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
if (empty($responseData['data']) || !is_array($responseData['data'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$data = $responseData['data'];
|
||||
|
||||
Log::info("Executed uid : {$uuid}");
|
||||
|
||||
// Use the static method from PbgTaskDetail model to create/update
|
||||
PbgTaskDetail::createFromApiResponse($data, $uuid);
|
||||
|
||||
return $responseData;
|
||||
} catch (\GuzzleHttp\Exception\ClientException $e) {
|
||||
if ($e->getCode() === 401 && !$retriedAfter401) {
|
||||
Log::warning("401 Unauthorized - Refreshing token and retrying...");
|
||||
try{
|
||||
$this->refreshToken();
|
||||
$options['headers']['Authorization'] = "Bearer {$this->user_token}";
|
||||
$retriedAfter401 = true;
|
||||
continue;
|
||||
}catch(\Exception $refreshError){
|
||||
Log::error("Token refresh and login failed: " . $refreshError->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (\GuzzleHttp\Exception\ServerException | \GuzzleHttp\Exception\ConnectException $e) {
|
||||
if ($e->getCode() === 502) {
|
||||
Log::warning("502 Bad Gateway - Retrying in {$initialDelay} seconds...");
|
||||
} else {
|
||||
Log::error("Network error ({$e->getCode()}) - Retrying in {$initialDelay} seconds...");
|
||||
}
|
||||
|
||||
sleep($initialDelay);
|
||||
$initialDelay *= 2;
|
||||
} catch (\GuzzleHttp\Exception\RequestException $e) {
|
||||
Log::error("Request error ({$e->getCode()}): " . $e->getMessage());
|
||||
return false;
|
||||
} catch (\JsonException $e) {
|
||||
Log::error("JSON decoding error: " . $e->getMessage());
|
||||
return false;
|
||||
} catch (\Throwable $e) {
|
||||
Log::critical("Unhandled error: " . $e->getMessage(), ['trace' => $e->getTraceAsString()]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Log::error("Failed to fetch task details for UUID {$uuid} after {$maxRetries} retries.");
|
||||
throw new \Exception("Failed to fetch task details for UUID {$uuid} after retries.");
|
||||
}
|
||||
|
||||
public function scraping_task_detail_status($uuid)
|
||||
{
|
||||
$url = "{$this->simbg_host}/api/pbg/v1/detail/{$uuid}/status/";
|
||||
$options = [
|
||||
'headers' => [
|
||||
'Authorization' => "Bearer {$this->user_token}",
|
||||
'Content-Type' => 'application/json'
|
||||
]
|
||||
];
|
||||
|
||||
$maxRetries = 3;
|
||||
$initialDelay = 1;
|
||||
$retriedAfter401 = false;
|
||||
|
||||
for ($retryCount = 0; $retryCount < $maxRetries; $retryCount++) {
|
||||
try {
|
||||
$response = $this->client->get($url, $options);
|
||||
$responseData = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
if (empty($responseData['data']) || !is_array($responseData['data'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$data = $responseData['data'];
|
||||
|
||||
Log::info("Executed uid : {$uuid}");
|
||||
|
||||
// Use the static method from PbgTaskDetail model to create/update
|
||||
PbgStatus::createOrUpdateFromApi($data, $uuid);
|
||||
|
||||
return $responseData;
|
||||
} catch (\GuzzleHttp\Exception\ClientException $e) {
|
||||
if ($e->getCode() === 401 && !$retriedAfter401) {
|
||||
Log::warning("401 Unauthorized - Refreshing token and retrying...");
|
||||
try{
|
||||
$this->refreshToken();
|
||||
$options['headers']['Authorization'] = "Bearer {$this->user_token}";
|
||||
$retriedAfter401 = true;
|
||||
continue;
|
||||
}catch(\Exception $refreshError){
|
||||
Log::error("Token refresh and login failed: " . $refreshError->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (\GuzzleHttp\Exception\ServerException | \GuzzleHttp\Exception\ConnectException $e) {
|
||||
if ($e->getCode() === 502) {
|
||||
Log::warning("502 Bad Gateway - Retrying in {$initialDelay} seconds...");
|
||||
} else {
|
||||
Log::error("Network error ({$e->getCode()}) - Retrying in {$initialDelay} seconds...");
|
||||
}
|
||||
|
||||
sleep($initialDelay);
|
||||
$initialDelay *= 2;
|
||||
} catch (\GuzzleHttp\Exception\RequestException $e) {
|
||||
Log::error("Request error ({$e->getCode()}): " . $e->getMessage());
|
||||
return false;
|
||||
} catch (\JsonException $e) {
|
||||
Log::error("JSON decoding error: " . $e->getMessage());
|
||||
return false;
|
||||
} catch (\Throwable $e) {
|
||||
Log::critical("Unhandled error: " . $e->getMessage(), ['trace' => $e->getTraceAsString()]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Log::error("Failed to fetch task detail status for UUID {$uuid} after {$maxRetries} retries.");
|
||||
throw new \Exception("Failed to fetch task details for UUID {$uuid} after retries.");
|
||||
}
|
||||
|
||||
public function scraping_task_assignments($uuid)
|
||||
{
|
||||
$url = "{$this->simbg_host}/api/pbg/v1/list-tim-penilai/{$uuid}/?page=1&size=10";
|
||||
$options = [
|
||||
@@ -164,7 +385,173 @@ class ServiceTabPbgTask
|
||||
throw new \Exception("Failed to fetch task assignments for UUID {$uuid} after retries.");
|
||||
}
|
||||
|
||||
private function scraping_task_retributions($uuid)
|
||||
public function scraping_pbg_data_list($uuid){
|
||||
$url = "{$this->simbg_host}/api/pbg/v1/detail/{$uuid}/list-data/?sort=DESC";
|
||||
$options = [
|
||||
'headers' => [
|
||||
'Authorization' => "Bearer {$this->user_token}",
|
||||
'Content-Type' => 'application/json'
|
||||
]
|
||||
];
|
||||
|
||||
$maxRetries = 3;
|
||||
$initialDelay = 1;
|
||||
$retriedAfter401 = false;
|
||||
|
||||
for ($retryCount = 0; $retryCount < $maxRetries; $retryCount++) {
|
||||
try{
|
||||
$response = $this->client->get($url, $options);
|
||||
$responseData = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
if (empty($responseData['data']) || !is_array($responseData['data'])) {
|
||||
Log::info("No data list found for UUID: {$uuid}");
|
||||
return true;
|
||||
}
|
||||
|
||||
$data = $responseData['data'];
|
||||
|
||||
Log::info("Processing data list for UUID: {$uuid}, found " . count($data) . " items");
|
||||
|
||||
// Process each data list item and save to database
|
||||
$this->processDataListItems($data, $uuid);
|
||||
|
||||
return $responseData;
|
||||
} catch (\GuzzleHttp\Exception\ClientException $e) {
|
||||
if ($e->getCode() === 401 && !$retriedAfter401) {
|
||||
Log::warning("401 Unauthorized - Refreshing token and retrying...");
|
||||
try{
|
||||
$this->refreshToken();
|
||||
$options['headers']['Authorization'] = "Bearer {$this->user_token}";
|
||||
$retriedAfter401 = true;
|
||||
continue;
|
||||
}catch(\Exception $refreshError){
|
||||
Log::error("Token refresh and login failed: " . $refreshError->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (\GuzzleHttp\Exception\ServerException | \GuzzleHttp\Exception\ConnectException $e) {
|
||||
if ($e->getCode() === 502) {
|
||||
Log::warning("502 Bad Gateway - Retrying in {$initialDelay} seconds...");
|
||||
} else {
|
||||
Log::error("Network error ({$e->getCode()}) - Retrying in {$initialDelay} seconds...");
|
||||
}
|
||||
|
||||
sleep($initialDelay);
|
||||
$initialDelay *= 2;
|
||||
} catch (\GuzzleHttp\Exception\RequestException $e) {
|
||||
Log::error("Request error ({$e->getCode()}): " . $e->getMessage());
|
||||
return false;
|
||||
} catch (\JsonException $e) {
|
||||
Log::error("JSON decoding error: " . $e->getMessage());
|
||||
return false;
|
||||
} catch (\Throwable $e) {
|
||||
Log::critical("Unhandled error: " . $e->getMessage(), ['trace' => $e->getTraceAsString()]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Log::error("Failed to fetch task data list for UUID {$uuid} after {$maxRetries} retries.");
|
||||
throw new \Exception("Failed to fetch task data list for UUID {$uuid} after retries.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Process and save data list items to database (Optimized with bulk operations)
|
||||
*/
|
||||
private function processDataListItems(array $dataListItems, string $pbgTaskUuid): void
|
||||
{
|
||||
try {
|
||||
if (empty($dataListItems)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$batchData = [];
|
||||
$validItems = 0;
|
||||
|
||||
foreach ($dataListItems as $item) {
|
||||
// Validate required fields
|
||||
if (empty($item['uid'])) {
|
||||
Log::warning("Skipping data list item with missing UID for PBG Task: {$pbgTaskUuid}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse created_at if exists
|
||||
$createdAt = null;
|
||||
if (!empty($item['created_at'])) {
|
||||
try {
|
||||
$createdAt = Carbon::parse($item['created_at'])->format('Y-m-d H:i:s');
|
||||
} catch (\Exception $e) {
|
||||
Log::warning("Invalid created_at format for data list UID: {$item['uid']}, Error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$batchData[] = [
|
||||
'uid' => $item['uid'],
|
||||
'name' => $item['name'] ?? null,
|
||||
'description' => $item['description'] ?? null,
|
||||
'status' => $item['status'] ?? null,
|
||||
'status_name' => $item['status_name'] ?? null,
|
||||
'data_type' => $item['data_type'] ?? null,
|
||||
'data_type_name' => $item['data_type_name'] ?? null,
|
||||
'file' => $item['file'] ?? null,
|
||||
'note' => $item['note'] ?? null,
|
||||
'pbg_task_uuid' => $pbgTaskUuid,
|
||||
'created_at' => $createdAt ?: now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
|
||||
$validItems++;
|
||||
}
|
||||
|
||||
if (!empty($batchData)) {
|
||||
// Use upsert for bulk insert/update operations
|
||||
PbgTaskDetailDataList::upsert(
|
||||
$batchData,
|
||||
['uid'], // Unique columns
|
||||
[
|
||||
'name', 'description', 'status', 'status_name',
|
||||
'data_type', 'data_type_name', 'file', 'note',
|
||||
'pbg_task_uuid', 'updated_at'
|
||||
] // Columns to update
|
||||
);
|
||||
|
||||
Log::info("Successfully bulk processed {$validItems} data list items for PBG Task: {$pbgTaskUuid}");
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error bulk processing data list items for PBG Task {$pbgTaskUuid}: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative method using PbgTask model's syncDataLists for cleaner code
|
||||
*/
|
||||
private function processDataListItemsWithModel(array $dataListItems, string $pbgTaskUuid): void
|
||||
{
|
||||
try {
|
||||
// Find the PbgTask
|
||||
$pbgTask = PbgTask::where('uuid', $pbgTaskUuid)->first();
|
||||
|
||||
if (!$pbgTask) {
|
||||
Log::error("PBG Task not found with UUID: {$pbgTaskUuid}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the model's syncDataLists method
|
||||
$pbgTask->syncDataLists($dataListItems);
|
||||
|
||||
$processedCount = count($dataListItems);
|
||||
Log::info("Successfully synced {$processedCount} data list items for PBG Task: {$pbgTaskUuid} using model method");
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error syncing data list items for PBG Task {$pbgTaskUuid}: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function scraping_task_retributions($uuid)
|
||||
{
|
||||
$url = "{$this->simbg_host}/api/pbg/v1/detail/" . $uuid . "/retribution/submit/";
|
||||
$options = [
|
||||
@@ -281,7 +668,7 @@ class ServiceTabPbgTask
|
||||
throw new \Exception("Failed to fetch task retributions for UUID {$uuid} after retries.");
|
||||
}
|
||||
|
||||
private function scraping_task_integrations($uuid){
|
||||
public function scraping_task_integrations($uuid){
|
||||
$url = "{$this->simbg_host}/api/pbg/v1/detail/" . $uuid . "/retribution/indeks-terintegrasi/";
|
||||
$options = [
|
||||
'headers' => [
|
||||
|
||||
79
app/Traits/HasRetributionCalculation.php
Normal file
79
app/Traits/HasRetributionCalculation.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use App\Models\RetributionCalculation;
|
||||
use App\Models\CalculableRetribution;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphOne;
|
||||
|
||||
trait HasRetributionCalculation
|
||||
{
|
||||
/**
|
||||
* Get all retribution calculations for this model (polymorphic many-to-many)
|
||||
*/
|
||||
public function retributionCalculations(): MorphMany
|
||||
{
|
||||
return $this->morphMany(CalculableRetribution::class, 'calculable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active retribution calculation
|
||||
*/
|
||||
public function activeRetributionCalculation(): MorphOne
|
||||
{
|
||||
return $this->morphOne(CalculableRetribution::class, 'calculable')
|
||||
->where('is_active', true)
|
||||
->latest('assigned_at');
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign calculation to this model
|
||||
*/
|
||||
public function assignRetributionCalculation(RetributionCalculation $calculation, string $notes = null): CalculableRetribution
|
||||
{
|
||||
// Deactivate previous active calculation
|
||||
$this->retributionCalculations()
|
||||
->where('is_active', true)
|
||||
->update(['is_active' => false]);
|
||||
|
||||
// Create new assignment
|
||||
return $this->retributionCalculations()->create([
|
||||
'retribution_calculation_id' => $calculation->id,
|
||||
'is_active' => true,
|
||||
'assigned_at' => now(),
|
||||
'notes' => $notes,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current retribution amount
|
||||
*/
|
||||
public function getCurrentRetributionAmount(): float
|
||||
{
|
||||
$activeCalculation = $this->activeRetributionCalculation;
|
||||
|
||||
return $activeCalculation
|
||||
? $activeCalculation->retributionCalculation->retribution_amount
|
||||
: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if has active calculation
|
||||
*/
|
||||
public function hasActiveRetributionCalculation(): bool
|
||||
{
|
||||
return $this->activeRetributionCalculation()->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get calculation history
|
||||
*/
|
||||
public function getRetributionCalculationHistory()
|
||||
{
|
||||
return $this->retributionCalculations()
|
||||
->with('retributionCalculation')
|
||||
->orderBy('assigned_at', 'desc')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
3
bootstrap/app.php
Executable file → Normal file
3
bootstrap/app.php
Executable file → Normal file
@@ -14,6 +14,9 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
$middleware->alias([
|
||||
'validate.api.token.web' => \App\Http\Middleware\ValidateApiTokenForWeb::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions) {
|
||||
$exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $th){
|
||||
|
||||
0
bootstrap/providers.php
Executable file → Normal file
0
bootstrap/providers.php
Executable file → Normal file
0
composer.json
Executable file → Normal file
0
composer.json
Executable file → Normal file
0
config/app.php
Executable file → Normal file
0
config/app.php
Executable file → Normal file
0
config/auth.php
Executable file → Normal file
0
config/auth.php
Executable file → Normal file
0
config/cache.php
Executable file → Normal file
0
config/cache.php
Executable file → Normal file
0
config/database.php
Executable file → Normal file
0
config/database.php
Executable file → Normal file
0
config/filesystems.php
Executable file → Normal file
0
config/filesystems.php
Executable file → Normal file
0
config/logging.php
Executable file → Normal file
0
config/logging.php
Executable file → Normal file
0
config/mail.php
Executable file → Normal file
0
config/mail.php
Executable file → Normal file
0
config/queue.php
Executable file → Normal file
0
config/queue.php
Executable file → Normal file
0
config/services.php
Executable file → Normal file
0
config/services.php
Executable file → Normal file
0
config/session.php
Executable file → Normal file
0
config/session.php
Executable file → Normal file
0
database/.gitignore
vendored
Executable file → Normal file
0
database/.gitignore
vendored
Executable file → Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user