Compare commits

..

105 Commits

Author SHA1 Message Date
arifal hidayat
91085e8796 fix duplicate insert when chunk 2025-09-14 20:25:36 +07:00
arifal
61e6eb9803 fix delete unused service 2025-09-12 17:31:18 +07:00
arifal
148dfebb4a fix relation name 2025-09-12 14:39:42 +07:00
arifal
aa34fff979 fix sidebar hover on collapsed 2025-09-12 14:14:32 +07:00
arifal
1a24b18719 fix collapse sidebar when active 2025-09-12 13:06:22 +07:00
arifal
e265e2ec35 fix sort order sidebar menu and percentage bigdata resume 2025-09-12 11:22:18 +07:00
arifal hidayat
e577da737b Merge branch 'dev' of 178.128.21.43:arifal/sibedas into dev 2025-09-11 23:33:35 +07:00
arifal hidayat
05ca927c38 fix pbg task payments 2025-09-11 23:30:43 +07:00
arifal
fc4b419878 fix pbg task payment 2025-09-11 13:17:55 +07:00
arifal hidayat
53d12d6798 add column full pbg task payments 2025-09-11 02:06:34 +07:00
arifal
809eb85255 fix menu seeder 2025-09-10 18:20:21 +07:00
arifal
8a513460bb remove log folder 2025-09-10 17:51:52 +07:00
arifal
fc74875cce ingore folder log 2025-09-10 17:49:18 +07:00
arifal
beb7d935c9 partial update fix redirect and add note in public, quick and pbg data 2025-09-10 17:24:44 +07:00
arifal
5c4cebd2b3 remove update target pad from spreadsheet 2025-09-08 13:07:17 +07:00
arifal hidayat
cbe3d00c96 fix type uid 2025-08-31 01:34:35 +07:00
arifal hidayat
65d9247b46 fix new hit endpoint pbg status 2025-08-31 01:22:51 +07:00
arifal
63310f2748 fix no data and handle default page load no data 2025-08-26 13:59:36 +07:00
arifal hidayat
c6257b79bf create public search 2025-08-26 02:33:09 +07:00
arifal hidayat
38493063c4 fix same column for quick-search table with pbg 2025-08-23 22:25:06 +07:00
arifal
954b2d8716 using bigdata 168 2025-08-21 11:31:01 +07:00
arifal
41cfce589b bigdata fix 169 2025-08-21 11:28:33 +07:00
arifal
8de1b51fea fix fix bapenda 2025-08-20 05:43:38 +07:00
arifal
fef6ae7522 fix data count and sum for showing 2025-08-20 05:06:45 +07:00
arifal
6f1cb4195a fix handle big svg 2025-08-20 02:37:42 +07:00
arifal
6a060f5dac fix handle inside system 2025-08-20 02:30:46 +07:00
arifal
844fbdfa89 fix line dashbaord 2025-08-20 02:14:50 +07:00
arifal
e18c0cb3b6 fix get current year 2025-08-20 00:34:26 +07:00
arifal
0a9d9071e4 fix update dashboard external 2025-08-20 00:23:30 +07:00
arifal
4b28bebcc2 fix pbg payments 2025-08-19 22:00:20 +07:00
arifal
1bcd2023da fix non business where unit is more than one 2025-08-19 19:02:54 +07:00
arifal
1b084ed485 add status spatial plannings 2025-08-19 18:15:58 +07:00
arifal
71ca8dc553 fix handle redirect rab circle 2025-08-19 14:33:40 +07:00
arifal
3cddd271c8 fix exclude is_valid false and clickable dashboard simbg 2025-08-19 14:06:07 +07:00
arifal
e9a70a827c add local backupdb 2025-08-19 13:22:20 +07:00
arifal
2cbc4172da fix pbg task add toggle and rab krk dlh 2025-08-19 13:00:40 +07:00
arifal hidayat
d7e9f44b20 fix render data and show count rab krk and dlh 2025-08-19 04:02:08 +07:00
arifal hidayat
7c7aa0e2a5 partial update filter data year now pbg 2025-08-19 02:25:03 +07:00
arifal hidayat
0111ab14e1 fix search like in quick search and pbg data 2025-08-19 01:40:28 +07:00
arifal
68e9d5eebf fix call scraping scheduler 2025-08-15 17:51:13 +07:00
arifal
209ef07f9c add new url scraping data and create tab data lists 2025-08-15 17:25:20 +07:00
arifal
6896fd62a3 fix handle duplicate data 2025-08-15 12:33:38 +07:00
root
b73183becf backupdb 2025-08-15 05:14:42 +00:00
arifal
9f9c3758ed try to fix count and show data 2025-08-15 12:06:27 +07:00
arifal hidayat
48a340d684 revised 2025-08-15 03:58:17 +07:00
arifal hidayat
3ff3dc8f17 fix verified 2025-08-15 03:53:42 +07:00
arifal hidayat
7936eb1dbf fix potention 2025-08-15 03:26:10 +07:00
arifal hidayat
ec047821a1 fix potention 2025-08-15 03:19:49 +07:00
arifal hidayat
e0ed007a39 fix business data count and show 2025-08-15 02:57:19 +07:00
arifal hidayat
bb63ea8084 fix business data 2025-08-15 02:51:17 +07:00
arifal hidayat
7a19d9f39d fix syncrone 2025-08-15 02:27:05 +07:00
arifal hidayat
93af7ab2a1 fix vite 2025-08-15 01:59:24 +07:00
arifal hidayat
6158903260 fix handle vite simple validator 2025-08-15 01:52:10 +07:00
arifal hidayat
09e7d41ddc fix asset 2025-08-15 01:41:36 +07:00
arifal hidayat
2f4ef6cb56 fix assets call 2025-08-15 01:37:52 +07:00
arifal
4f94e9d8f7 add dump backupdb 2025-08-14 16:26:46 +00:00
arifal hidayat
fa6a0079dc fix handle redirect and add filter data pbg task same with dashboard 2025-08-14 23:11:36 +07:00
arifal
f7497cbec8 partial update count and sum dashboard 2025-08-14 20:00:23 +07:00
arifal
b5f7bf39b2 add spatial planning count and sum on dashboard simbg 2025-08-08 15:11:32 +07:00
arifal hidayat
ef3c9d6fc3 fix total non business count and sum 2025-08-08 03:53:38 +07:00
arifal hidayat
1288ab509d fix data and dashboard count and sum 2025-08-08 03:17:04 +07:00
arifal hidayat
588e3ad5e2 add backup success sync 2025-08-08 00:12:22 +07:00
arifal
3902a486f7 partial update fix sync data pbg 2025-08-07 22:04:40 +07:00
arifal hidayat
dd1cd72450 add build 2025-08-07 00:53:32 +07:00
arifal hidayat
af05a39a82 fix menu tax in data and fix session when multiple user login 2025-08-07 00:51:46 +07:00
arifal hidayat
0abf278aa3 add edit and delete data tax 2025-08-06 00:36:56 +07:00
arifal
c2cb1b99f2 fix searching using registration number only and zip build frontend 2025-08-05 14:51:56 +07:00
arifal hidayat
7135876ebc create page tax with data, upload and export group by subdistrict 2025-08-05 01:30:37 +07:00
arifal
456eec83dc fix index default sheet Bagan 2025 on spreadsheet 2025-07-18 14:00:20 +07:00
arifal
6a22b55a1c fix calculated and truncate calculation spatial plannings 2025-07-03 19:19:20 +07:00
arifal
5aab6fa3d1 add 10% 2025-07-03 15:34:02 +07:00
arifal
a1e302a56d add 30% 2025-07-03 15:28:54 +07:00
arifal
a7f578ca3d add docker for server demo 2025-06-26 18:28:26 +07:00
arifal
c33193d5f0 add docker 2025-06-24 15:09:21 +07:00
arifal
2c7c99bcf1 add backup db from demo server 2025-06-24 13:45:04 +07:00
arifal
a01b6f5611 fix error sync google sheet 2025-06-24 13:00:48 +07:00
arifal
2f43ebe97e build 2025-06-24 12:05:00 +07:00
arifal
e5baf5318f add separator in js for handle showing value is number 2025-06-24 12:02:17 +07:00
arifal
b895f61701 fix duplicate calculation ids 2025-06-23 18:20:16 +07:00
arifal
5dd92aa323 create new command for insert init spatial plannings dan remove unused retribution tables 2025-06-23 17:52:55 +07:00
arifal
7eb5a850c2 add new build vite 2025-06-23 13:56:42 +07:00
arifal
200b398868 fix handle null on scraping google sheet data and add detail data building 2025-06-23 13:54:26 +07:00
arifal
ccff82bd22 add build js 2025-06-19 13:57:28 +07:00
arifal
285e89d5d0 done restructure calculation retribution 2025-06-19 13:48:35 +07:00
arifal hidayat
4c3443c2d6 restructure retribution calculations table 2025-06-18 22:53:44 +07:00
arifal
df70a47bd1 add build 2025-06-18 15:45:12 +07:00
arifal
e71dd7d213 count spatial plannings business and non business and create pbg task detail and add to syncrone daily 2025-06-18 13:45:35 +07:00
arifal hidayat
f2eb998ac5 build and add init spatial 2025-06-18 02:57:30 +07:00
arifal hidayat
fc54e20fa4 add spatial plannings retribution calculations 2025-06-18 02:54:41 +07:00
arifal
6946fa7074 update retribution calculation spatial plannings 2025-06-17 17:58:37 +07:00
arifal
236b6f9bfc add build frontend 2025-06-17 12:04:55 +07:00
arifal
285ff46c2b fix data setting get datatable using api token 2025-06-17 11:54:02 +07:00
arifal hidayat
a8b02afad9 add build production manifest 2025-06-14 04:22:57 +07:00
arifal hidayat
a0666e78d2 fix value dashboard verified component 2025-06-14 04:20:26 +07:00
arifal hidayat
799e409ce2 create build script 2025-06-14 02:48:00 +07:00
arifal hidayat
780ba60224 add sync to leader dashboard new from google spreadsheet 2025-06-14 02:15:49 +07:00
arifal
baed8cc487 fix 401 hit api 2025-06-13 20:43:16 +07:00
arifal
e17f5beaf0 production code 2025-06-13 13:36:27 +00:00
arifal
766e1a430c fix permision deploy 2025-06-13 20:08:14 +07:00
arifal
6677c320fc build and create deploy production 2025-06-13 19:53:23 +07:00
arifal hidayat
9437eb949f add docker 2025-06-06 22:42:41 +07:00
arifal
6f77120c33 backup database 2025-06-02 15:41:03 +07:00
arifal
f8d0573e5c remove description body response on datatable 2025-06-02 15:30:43 +07:00
arifal
ca74d0143f hot fix show all chart growth 2025-05-20 11:59:50 +07:00
arifal
34e082c31b fix bug sync google sheet and handle to long data address on pbg task 2025-05-20 11:13:21 +07:00
407 changed files with 18265 additions and 5059 deletions

0
.editorconfig Executable file → Normal file
View File

3
.env.example Executable file → Normal file
View File

@@ -2,7 +2,7 @@ APP_NAME=Laravel
APP_ENV=local APP_ENV=local
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=true
APP_TIMEZONE=UTC APP_TIMEZONE=Asia/Jakarta
APP_URL=http://localhost APP_URL=http://localhost
API_URL=http://localhost:8000 API_URL=http://localhost:8000
@@ -65,6 +65,7 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}" VITE_APP_NAME="${APP_NAME}"
VITE_APP_HOST=localhost
API_KEY_GOOGLE="xxxxx" API_KEY_GOOGLE="xxxxx"
SPREAD_SHEET_ID="xxxxx" SPREAD_SHEET_ID="xxxxx"

0
.gitattributes vendored Executable file → Normal file
View File

3
.gitignore vendored Executable file → Normal file
View File

@@ -21,3 +21,6 @@ yarn-error.log
/.nova /.nova
/.vscode /.vscode
/.zed /.zed
/.composer
/.config
/.npm

1
.lesshst Normal file
View File

@@ -0,0 +1 @@
.less-history-file:

115
Dockerfile Normal file
View 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
View 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
``` - Docker & Docker Compose
sudo apt update && sudo apt install supervisor -y - 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
``` ```bash
sudo nano /etc/supervisor/conf.d/laravel-worker.conf # 1. Setup environment
cp env.production.example .env
nano .env
[program:laravel-worker] # 2. Deploy with SSL (Recommended)
process_name=%(program_name)s_%(process_num)02d ./scripts/setup-reverse-proxy.sh setup
command=php /home/arifal/development/sibedas-pbg-web/artisan queue:work --queue=default --timeout=82800 --tries=1
autostart=true # 3. Check status
autorestart=true ./scripts/setup-reverse-proxy.sh status
numprocs=1 # Access: https://yourdomain.com
redirect_stderr=true
stdout_logfile=/home/arifal/development/sibedas-pbg-web/storage/logs/worker.log
stopasgroup=true
killasgroup=true
``` ```
- Reload Supervisor ## 🏗️ Architecture
### Local Development
``` ```
sudo supervisorctl reread Browser → Port 8000 → Nginx → PHP-FPM → MariaDB
sudo supervisorctl update
sudo supervisorctl start laravel-worker
sudo supervisorctl restart laravel-worker
sudo supervisorctl status
``` ```
# How to running ### Production dengan Reverse Proxy
- Install composer package
``` ```
composer install Internet → Reverse Proxy (80/443) → Internal Nginx → PHP-FPM → MariaDB
``` ```
- Install npm package ## 🔧 Configuration
``` ### Environment Variables
npm install && npm run build
```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
``` ### 1. Server Preparation
php artisan storage:link
```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
``` ```bash
php artisan migrate git clone <repository-url>
cd sibedas-pbg-web
chmod +x scripts/*.sh
cp env.production.example .env
nano .env
``` ```
- Running seeder ### 3. Deploy
``` ```bash
php artisan db:seed # 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 ### 4. Verify
- excute all sql queries on folder database/view_query
# Add ENV variable ```bash
docker-compose ps
- API_KEY_GOOGLE ./scripts/setup-reverse-proxy.sh status
curl -f http://localhost/health-check
```
Get api key from google developer console for and turn on spreadsheet api or feaature for google sheet
``` ```
- SPREAD_SHEET_ID ## 📊 Monitoring
``` ```bash
Get spreadsheet id from google sheet link # 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
``` ```bash
Get OpenAI API key from chatgpt subscription # 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" - `scripts/setup-reverse-proxy.sh` - Setup lengkap reverse proxy dan SSL
SPREAD_SHEET_ID="xxxxx" - `scripts/deploy-production.sh` - Deployment production
OPENAI_API_KEY="xxxxx" - `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 ## 📚 Documentation
- Laravel 11
- node v22.13.0 Untuk dokumentasi lengkap, lihat [docs/README.md](docs/README.md)
- npm 10.9.2
- mariadb Ver 15.1 Distrib 10.6.18-MariaDB, for debian-linux-gnu (x86_64) using EditLine wrapper ## 🆘 Support
- Ubuntu 24.04
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/`

View 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}");
}
$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,
]
];
}
}
}

View File

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

View 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;
}
}

View File

@@ -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()
// ]);
// }
// }
// }
}

View 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;
}
}

View 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()
]);
}
}
}
}

View 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;
}
}

View 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;
}
}
}

View 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;
}
}

View 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;');
}
}

View 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');
}
}

View File

@@ -9,14 +9,32 @@ enum PbgTaskFilterData : string
case verified = 'verified'; case verified = 'verified';
case non_verified = 'non-verified'; case non_verified = 'non-verified';
case all = 'all'; 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 { public static function getAllOptions() : array {
return [ return [
self::all->value => 'Potensi Berkas', self::all->value => 'Semua Berkas',
self::business->value => 'Usaha', self::business->value => 'Usaha',
self::non_business->value => 'Bukan Usaha', self::non_business->value => 'Bukan Usaha',
self::verified->value => 'Terverifikasi', self::verified->value => 'Terverifikasi',
self::non_verified->value => 'Belum 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',
]; ];
} }
} }

View File

@@ -56,4 +56,90 @@ enum PbgTaskStatus: int
{ {
return self::getStatuses()[$status] ?? null; 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
];
}
} }

View 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',
];
}
}

View 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);
}
}

View 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;
}
}

View File

@@ -8,6 +8,8 @@ use App\Http\Controllers\Controller;
use App\Http\Resources\BigdataResumeResource; use App\Http\Resources\BigdataResumeResource;
use App\Models\BigdataResume; use App\Models\BigdataResume;
use App\Models\DataSetting; use App\Models\DataSetting;
use App\Models\SpatialPlanning;
use App\Models\PbgTaskPayment;
use Barryvdh\DomPDF\Facade\Pdf; use Barryvdh\DomPDF\Facade\Pdf;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -23,14 +25,16 @@ class BigDataResumeController extends Controller
{ {
try{ try{
$filterDate = $request->get("filterByDate"); $filterDate = $request->get("filterByDate");
$type = trim($request->get("type"));
if (!$filterDate || $filterDate === "latest") { 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) { if (!$big_data_resume) {
return $this->response_empty_resume(); return $this->response_empty_resume();
} }
} else { } else {
$big_data_resume = BigdataResume::whereDate('created_at', $filterDate) $big_data_resume = BigdataResume::whereDate('created_at', $filterDate)
->where('resume_type', $type)
->orderBy('id', 'desc') ->orderBy('id', 'desc')
->first(); ->first();
@@ -40,23 +44,28 @@ class BigDataResumeController extends Controller
} }
$data_settings = DataSetting::all(); $data_settings = DataSetting::all();
if($data_settings->isEmpty()){ $target_pad = 0;
return response()->json(['message' => 'No data setting found']); if($data_settings->where('key', 'TARGET_PAD')->first()){
$target_pad = floatval($data_settings->where('key', 'TARGET_PAD')->first()->value ?? 0);
} }
function cleanNumber($value) { $realisasi_terbit_pbg_sum = $big_data_resume->issuance_realization_pbg_sum;
return floatval(str_replace('.', '', $value)); $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); // Get real-time spatial planning data using new calculation formula
$realisasi_terbit_pbg_sum = cleanNumber(optional($data_settings->where('key', 'REALISASI_TERBIT_PBG_SUM')->first())->value); $spatialData = $this->getSpatialPlanningData();
$realisasi_terbit_pbg_count = cleanNumber(optional($data_settings->where('key', 'REALISASI_TERBIT_PBG_COUNT')->first())->value); $tata_ruang = $spatialData['sum'];
$menunggu_klik_dpmptsp_sum = cleanNumber(optional($data_settings->where('key', 'MENUNGGU_KLIK_DPMPTSP_SUM')->first())->value); $tata_ruang_count = $spatialData['count'];
$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); // Get real-time PBG Task Payments data
$proses_dinas_teknis_count = cleanNumber(optional($data_settings->where('key', 'PROSES_DINAS_TEKNIS_COUNT')->first())->value); $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; $kekurangan_potensi = $target_pad - $big_data_resume->potention_sum;
// percentage kekurangan potensi // percentage kekurangan potensi
@@ -67,46 +76,62 @@ class BigDataResumeController extends Controller
$total_potensi_percentage = $big_data_resume->potention_sum > 0 && $target_pad > 0 $total_potensi_percentage = $big_data_resume->potention_sum > 0 && $target_pad > 0
? round(($big_data_resume->potention_sum / $target_pad) * 100, 2) : 0; ? round(($big_data_resume->potention_sum / $target_pad) * 100, 2) : 0;
// percentage verified document // // percentage verified document (verified_sum / potention_sum) - by value/amount
$verified_percentage = $big_data_resume->verified_sum > 0 && $big_data_resume->potention_sum > 0 // $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; // ? round(($big_data_resume->verified_sum / $big_data_resume->potention_sum) * 100, 2) : 0;
// percentage non-verified document // // percentage non-verified document (non_verified_sum / potention_sum) - by value/amount
$non_verified_percentage = $big_data_resume->non_verified_sum > 0 && $big_data_resume->potention_sum > 0 // $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; // ? round(($big_data_resume->non_verified_sum / $big_data_resume->potention_sum) * 100, 2) : 0;
// percentage business document // Alternative: percentage by count (if needed)
$business_percentage = $big_data_resume->business_sum > 0 && $big_data_resume->non_verified_sum > 0 $verified_count_percentage = $big_data_resume->potention_count > 0 && $big_data_resume->verified_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 && $big_data_resume->non_verified_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; ? round(($big_data_resume->business_sum / $big_data_resume->non_verified_sum) * 100, 2) : 0;
// percentage non-business document // percentage non-business document (non_business / non_verified)
$non_business_percentage = $big_data_resume->non_business_sum > 0 && $big_data_resume->potention_sum > 0 $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->potention_sum) * 100, 2) : 0; ? round(($big_data_resume->non_business_sum / $big_data_resume->non_verified_sum) * 100, 2) : 0;
// percentage tata ruang // percentage tata ruang (spatial / potention)
$tata_ruang_percentage = $tata_ruang > 0 && $big_data_resume->potention_sum > 0 $tata_ruang_percentage = $big_data_resume->potention_sum > 0 && $tata_ruang >= 0
? round(($tata_ruang / $big_data_resume->potention_sum) * 100, 2) : 0; ? round(($tata_ruang / $big_data_resume->potention_sum) * 100, 2) : 0;
// percentage realisasi terbit pbg // percentage realisasi terbit pbg (issuance / verified)
$realisasi_terbit_percentage = $big_data_resume->verified_sum > 0 && $realisasi_terbit_pbg_sum > 0 $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; ? round(($realisasi_terbit_pbg_sum / $big_data_resume->verified_sum) * 100, 2) : 0;
// percentage menunggu klik dpmptsp // percentage menunggu klik dpmptsp (waiting / verified)
$menunggu_klik_dpmptsp_percentage = $big_data_resume->verified_sum > 0 && $menunggu_klik_dpmptsp_sum > 0 $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; ? round(($menunggu_klik_dpmptsp_sum / $big_data_resume->verified_sum) * 100, 2) : 0;
// percentage proses_dinas_teknis // percentage proses_dinas_teknis (process / verified)
$proses_dinas_teknis_percentage = $big_data_resume->verified_sum > 0 && $proses_dinas_teknis_sum > 0 $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; ? 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 = [ $result = [
'target_pad' => [ 'target_pad' => [
'sum' => $target_pad, 'sum' => $target_pad,
'percentage' => 100, 'percentage' => 100,
], ],
'tata_ruang' => [ 'tata_ruang' => [
'sum' => $big_data_resume->spatial_sum, 'sum' => $tata_ruang,
'count' => $big_data_resume->spatial_count, 'count' => $tata_ruang_count,
'percentage' => $tata_ruang_percentage, 'percentage' => $tata_ruang_percentage,
], ],
'kekurangan_potensi' => [ 'kekurangan_potensi' => [
@@ -121,12 +146,12 @@ class BigDataResumeController extends Controller
'verified_document' => [ 'verified_document' => [
'sum' => (float) $big_data_resume->verified_sum, 'sum' => (float) $big_data_resume->verified_sum,
'count' => $big_data_resume->verified_count, 'count' => $big_data_resume->verified_count,
'percentage' => $verified_percentage 'percentage' => $verified_count_percentage
], ],
'non_verified_document' => [ 'non_verified_document' => [
'sum' => (float) $big_data_resume->non_verified_sum, 'sum' => (float) $big_data_resume->non_verified_sum,
'count' => $big_data_resume->non_verified_count, 'count' => $big_data_resume->non_verified_count,
'percentage' => $non_verified_percentage 'percentage' => $non_verified_count_percentage
], ],
'business_document' => [ 'business_document' => [
'sum' => (float) $big_data_resume->business_sum, 'sum' => (float) $big_data_resume->business_sum,
@@ -152,6 +177,16 @@ class BigDataResumeController extends Controller
'sum' => $proses_dinas_teknis_sum, 'sum' => $proses_dinas_teknis_sum,
'count' => $proses_dinas_teknis_count, 'count' => $proses_dinas_teknis_count,
'percentage' => $proses_dinas_teknis_percentage '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); return response()->json($result);
@@ -321,9 +356,15 @@ class BigDataResumeController extends Controller
return $pdf->download('laporan-pimpinan.pdf'); return $pdf->download('laporan-pimpinan.pdf');
} }
private function response_empty_resume(){ 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 = [ $result = [
'target_pad' => [ 'target_pad' => [
'sum' => 0, 'sum' => $target_pad,
'percentage' => 100, 'percentage' => 100,
], ],
'tata_ruang' => [ 'tata_ruang' => [
@@ -373,9 +414,97 @@ class BigDataResumeController extends Controller
'sum' => 0, 'sum' => 0,
'count' => 0, 'count' => 0,
'percentage' => 0 'percentage' => 0
],
'pbg_task_payments' => [
'sum' => 0,
'count' => 0,
'percentage' => 0
] ]
]; ];
return response()->json($result); 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,
];
}
}
} }

View File

@@ -9,6 +9,7 @@ use App\Http\Resources\CustomersResource;
use App\Imports\CustomersImport; use App\Imports\CustomersImport;
use App\Models\Customer; use App\Models\Customer;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Maatwebsite\Excel\Facades\Excel; use Maatwebsite\Excel\Facades\Excel;
class CustomersController extends Controller class CustomersController extends Controller
@@ -120,7 +121,7 @@ class CustomersController extends Controller
'message' => 'File uploaded successfully', 'message' => 'File uploaded successfully',
]); ]);
}catch(\Exception $e){ }catch(\Exception $e){
\Log::info($e->getMessage()); Log::info($e->getMessage());
return response()->json([ return response()->json([
'error' => 'Failed to upload file', 'error' => 'Failed to upload file',
'message' => $e->getMessage() 'message' => $e->getMessage()

View File

@@ -23,19 +23,25 @@ class GrowthReportAPIController extends Controller
$defaultEnd = $today; $defaultEnd = $today;
// Use request values if provided, else use defaults // Use request values if provided, else use defaults
$startDate = $request->input('start_date', $defaultStart->toDateString()); // $startDate = $request->input('start_date', $defaultStart->toDateString());
$endDate = $request->input('end_date', $defaultEnd->toDateString()); // $endDate = $request->input('end_date', $defaultEnd->toDateString());
// Optional year filter (used if specified) // Optional year filter (used if specified)
$year = $request->input('year', now()->year); $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(" $query = BigdataResume::selectRaw("
DATE(created_at) as date, DATE(created_at) as date,
SUM(potention_sum) as potention_sum, SUM(potention_sum) as potention_sum,
SUM(verified_sum) as verified_sum, SUM(verified_sum) as verified_sum,
SUM(non_verified_sum) as non_verified_sum SUM(non_verified_sum) as non_verified_sum
") ");
->whereBetween('created_at', [$startDate, $endDate]);
$query->whereNotNull('year') $query->whereNotNull('year')
->where('year', '!=', 'all'); ->where('year', '!=', 'all');

View File

@@ -8,6 +8,8 @@ use App\Models\Customer;
use App\Models\SpatialPlanning; use App\Models\SpatialPlanning;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Models\TourismBasedKBLI; use App\Models\TourismBasedKBLI;
use App\Models\Tax;
use Illuminate\Support\Facades\Log;
class LackOfPotentialController extends Controller class LackOfPotentialController extends Controller
{ {
@@ -17,13 +19,28 @@ class LackOfPotentialController extends Controller
$total_reklame = Advertisement::count(); $total_reklame = Advertisement::count();
$total_pdam = Customer::count(); $total_pdam = Customer::count();
$total_tata_ruang = SpatialPlanning::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_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([ return response()->json([
'total_reklame' => $total_reklame, 'total_reklame' => $total_reklame,
'total_pdam' => $total_pdam, 'total_pdam' => $total_pdam,
'total_tata_ruang' => $total_tata_ruang, '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_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); ], 200);
}catch(\Exception $e){ }catch(\Exception $e){
return response()->json([ return response()->json([
@@ -31,4 +48,63 @@ class LackOfPotentialController extends Controller
], 500); ], 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,
];
}
}
} }

View File

@@ -12,7 +12,6 @@ use App\Models\DataSetting;
use App\Models\ImportDatasource; use App\Models\ImportDatasource;
use App\Models\PbgTask; use App\Models\PbgTask;
use App\Models\PbgTaskGoogleSheet; use App\Models\PbgTaskGoogleSheet;
use App\Services\GoogleSheetService;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@@ -20,10 +19,6 @@ use Illuminate\Validation\Rules\Enum;
class PbgTaskController extends Controller class PbgTaskController extends Controller
{ {
protected $googleSheetService;
public function __construct(GoogleSheetService $googleSheetService){
$this->googleSheetService = $googleSheetService;
}
public function index(Request $request) public function index(Request $request)
{ {
info($request); info($request);
@@ -132,39 +127,48 @@ class PbgTaskController extends Controller
} }
$validated = $request->validate([ $validated = $request->validate([
'name' => 'required|string|max:255', 'name' => 'nullable|string|max:255',
'owner_name' => 'required|string|max:255', 'owner_name' => 'nullable|string|max:255',
'application_type' => ['nullable', new Enum(PbgTaskApplicationTypes::class)], 'application_type' => ['nullable', new Enum(PbgTaskApplicationTypes::class)],
'condition' => 'required|string|max:255', 'condition' => 'nullable|string|max:255',
'registration_number' => 'required|string|max:255', 'registration_number' => 'nullable|string|max:255',
'document_number' => 'required|string|max:255', 'document_number' => 'nullable|string|max:255',
'status' => ['nullable', new Enum(PbgTaskStatus::class)], 'status' => ['nullable', new Enum(PbgTaskStatus::class)],
'address' => 'required|string|max:255', 'address' => 'nullable|string|max:255',
'slf_status_name' => 'nullable|string|max:255', 'slf_status_name' => 'nullable|string|max:255',
'function_type' => 'required|string|max:255', 'function_type' => 'nullable|string|max:255',
'consultation_type' => 'required|string|max:255', 'consultation_type' => 'nullable|string|max:255',
'due_date' => 'nullable|date|after_or_equal:today', 'due_date' => 'nullable|date',
'is_valid' => 'nullable|boolean',
]); ]);
$statusLabel = $validated['status'] !== null ? PbgTaskStatus::getLabel($validated['status']) : null; $statusLabel = $validated['status'] !== null ? PbgTaskStatus::getLabel($validated['status']) : null;
$applicationLabel = $validated['application_type'] !== null ? PbgTaskApplicationTypes::getLabel($validated['application_type']) : null; $applicationLabel = $validated['application_type'] !== null ? PbgTaskApplicationTypes::getLabel($validated['application_type']) : null;
$pbg_task->update([ // Prepare update data - only include fields that are actually provided
'name' => $validated['name'], $updateData = [];
'owner_name' => $validated['owner_name'],
'application_type' => $validated['application_type'], foreach ($validated as $key => $value) {
'application_type_name' => $applicationLabel, // Automatically set application_type_name if ($value !== null || $request->has($key)) {
'condition' => $validated['condition'], $updateData[$key] = $value;
'registration_number' => $validated['registration_number'], }
'document_number' => $validated['document_number'], }
'status' => $validated['status'],
'status_name' => $statusLabel, // Automatically set status_name // Handle special cases for labels
'address' => $validated['address'], if (isset($updateData['status'])) {
'slf_status_name' => $validated['slf_status_name'], $updateData['status_name'] = $statusLabel;
'function_type' => $validated['function_type'], }
'consultation_type' => $validated['consultation_type'],
'due_date' => $validated['due_date'], 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([ return response()->json([
"success"=> true, "success"=> true,
"message"=> "Data berhasil diubah", "message"=> "Data berhasil diubah",

View File

@@ -11,8 +11,9 @@ use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Exception; use Exception;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Log; use Illuminate\Support\Facades\Log;
use Maatwebsite\Excel\Facades\Excel; use Maatwebsite\Excel\Facades\Excel;
use App\Enums\PbgTaskStatus;
class RequestAssignmentController extends Controller class RequestAssignmentController extends Controller
{ {
@@ -21,52 +22,365 @@ class RequestAssignmentController extends Controller
*/ */
public function index(Request $request) 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) { 'attachments' => function ($q) {
$q->whereIn('pbg_type', ['berita_acara', 'bukti_bayar']); $q->whereIn('pbg_type', ['berita_acara', 'bukti_bayar']);
}, },
'googleSheet' 'pbg_task_retributions',
'pbg_task_detail',
'pbg_status'
])->orderBy('id', 'desc'); ])->orderBy('id', 'desc');
if ($request->has('filter') && !empty($request->get('filter'))) { // Log final query count for debugging
$filter = strtolower($request->get('filter')); 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()
]);
switch ($filter) { // Cross-validation with BigdataResume logic (for debugging consistency)
case 'non-business': if ($filter !== 'all' && $request->has('year') && !empty($request->get('year'))) {
$query->whereRaw("LOWER(function_type) != ?", ['sebagai tempat usaha'])->orWhereNull('function_type'); $this->validateConsistencyWithBigdataResume($filter, $request->get('year'), $accurateCount);
break;
case 'business':
$query->whereRaw("LOWER(function_type) = ?", ['sebagai tempat usaha']);
break;
case 'verified':
$query->whereHas('googleSheet', function ($q) {
$q->whereRaw("LOWER(status_verifikasi) = ?", ['selesai verifikasi']);
});
break;
case 'non-verified':
$query->where(function ($q) {
$q->whereDoesntHave('googleSheet')
->orWhereHas('googleSheet', function ($q2) {
$q2->whereRaw("LOWER(status_verifikasi) != ?", ['selesai verifikasi'])->orWhereNull('status_verifikasi');
});
});
break;
}
} }
// Apply search to data query
if ($request->has('search') && !empty($request->get("search"))) { if ($request->has('search') && !empty($request->get("search"))) {
$search = $request->get('search'); $this->applySearch($dataQuery, $request->get('search'));
$query->where(function ($q) use ($search) {
$q->where('name', 'LIKE', "%$search%")
->orWhere('registration_number', 'LIKE', "%$search%")
->orWhere('document_number', 'LIKE', "%$search%");
});
} }
return RequestAssignmentResouce::collection($query->paginate()); // 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':
// 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':
// 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':
// 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->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_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', 5)
->where('pbg_task_detail_data_lists.status', '!=', 1);
});
break;
default:
// Log unrecognized filter for debugging
Log::warning('Unrecognized filter value', ['filter' => $filter]);
break;
}
}
/**
* 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('owner_name', 'LIKE', "%$search%")
->orWhere('address', 'LIKE', "%$search%");
});
// 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) 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_detail', 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_detail', function ($q4) {
$q4->where('unit', '>', 1);
})
->orWhereDoesntHave('pbg_task_detail');
});
})
->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
]);
}
}
} }

View File

@@ -26,6 +26,10 @@ class SpatialPlanningController extends Controller
$search = $request->input('search', ''); $search = $request->input('search', '');
$query = SpatialPlanning::query(); $query = SpatialPlanning::query();
// Only include spatial plannings that are not yet issued (is_terbit = false)
$query->where('is_terbit', false);
if ($search) { if ($search) {
$query->where(function ($q) use ($search) { $query->where(function ($q) use ($search) {
$q->where('name', 'like', "%$search%") $q->where('name', 'like', "%$search%")
@@ -42,9 +46,11 @@ class SpatialPlanningController extends Controller
// Menambhakan nomor urut (No) // Menambhakan nomor urut (No)
$start = ($spatialPlannings->currentPage()-1) * $perPage + 1; $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) { $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); info($data);
@@ -104,9 +110,10 @@ class SpatialPlanningController extends Controller
/** /**
* Display the specified resource. * 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();
} }
/** /**

View 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);
}
}
}

View File

@@ -36,7 +36,9 @@ class UsersController extends Controller
return UserResource::collection($query->paginate(config('app.paginate_per_page', 50))); return UserResource::collection($query->paginate(config('app.paginate_per_page', 50)));
} }
public function logout(Request $request){ 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']); return response()->json(['message' => 'logged out successfully']);
} }
public function store(UsersRequest $request){ public function store(UsersRequest $request){

View File

@@ -36,11 +36,77 @@ class AuthenticatedSessionController extends Controller
// Ambil user yang sedang login // Ambil user yang sedang login
$user = Auth::user(); $user = Auth::user();
// Buat token untuk API // Hapus token lama jika ada
$token = $user->createToken(env('APP_KEY'))->plainTextToken; \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]); 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) public function destroy(Request $request)
{ {
if($request->user()){ 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(); Auth::guard('web')->logout();

View File

View File

View File

0
app/Http/Controllers/Auth/NewPasswordController.php Executable file → Normal file
View File

View File

0
app/Http/Controllers/Auth/RegisteredUserController.php Executable file → Normal file
View File

0
app/Http/Controllers/Auth/VerifyEmailController.php Executable file → Normal file
View File

0
app/Http/Controllers/Controller.php Executable file → Normal file
View File

View File

@@ -21,4 +21,13 @@ class BigDataController extends Controller
{ {
return view('dashboards.pbg'); 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'));
}
} }

View File

@@ -82,9 +82,15 @@ class SpatialPlanningController extends Controller
"kbli"=> "KBLI", "kbli"=> "KBLI",
"activities"=> "Kegiatan", "activities"=> "Kegiatan",
"area"=> "Luas (m2)", "area"=> "Luas (m2)",
"land_area"=> "Luas Lahan (m2)",
"location"=> "Lokasi", "location"=> "Lokasi",
"number"=> "Nomor", "number"=> "Nomor",
"date"=> "Tanggal", "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", "kbli"=> "text",
"activities"=> "text", "activities"=> "text",
"area"=> "text", "area"=> "text",
"land_area"=> "text",
"location"=> "text", "location"=> "text",
"number"=> "text", "number"=> "text",
"date"=> "date", "date"=> "date",
"site_bcr"=> "text",
"building_function"=> "text",
"business_type_info"=> "readonly",
"is_terbit"=> "select",
"calculated_retribution"=> "readonly",
]; ];
} }
} }

View File

@@ -8,6 +8,8 @@ use App\Http\Resources\TaskAssignmentsResource;
use App\Models\PbgTask; use App\Models\PbgTask;
use App\Models\TaskAssignment; use App\Models\TaskAssignment;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
class QuickSearchController extends Controller class QuickSearchController extends Controller
{ {
@@ -15,6 +17,10 @@ class QuickSearchController extends Controller
return view("quick-search.index"); return view("quick-search.index");
} }
public function public_search(){
return view("public-search.index");
}
public function search_result(Request $request){ public function search_result(Request $request){
$keyword = $request->get("keyword"); $keyword = $request->get("keyword");
@@ -24,21 +30,105 @@ class QuickSearchController extends Controller
public function quick_search_datatable(Request $request) public function quick_search_datatable(Request $request)
{ {
try { 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')) { if ($request->filled('search')) {
$search = $request->get('search'); $search = trim($request->get('search'));
$query->where(function ($q) use ($search) { $query->where(function ($q) use ($search) {
$q->where('name', 'LIKE', "%$search%") $q->where('pbg_task.registration_number', 'LIKE', "%$search%")
->orWhere('registration_number', 'LIKE', "%$search%") ->orWhere('pbg_task.name', 'LIKE', "%$search%")
->orWhere('address', 'LIKE', "%$search%") ->orWhere('pbg_task.owner_name', 'LIKE', "%$search%")
->orWhere('document_number', '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()); return response()->json($query->paginate());
} catch (\Throwable $e) { } 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([ return response()->json([
'message' => 'Terjadi kesalahan saat mengambil data.', 'message' => 'Terjadi kesalahan saat mengambil data.',
'error' => $e->getMessage(), 'error' => $e->getMessage(),
@@ -52,7 +142,8 @@ class QuickSearchController extends Controller
$data = PbgTask::with([ $data = PbgTask::with([
'pbg_task_retributions', 'pbg_task_retributions',
'pbg_task_index_integrations', 'pbg_task_index_integrations',
'pbg_task_retributions.pbg_task_prasarana' 'pbg_task_retributions.pbg_task_prasarana',
'pbg_status'
])->findOrFail($id); ])->findOrFail($id);
$statusOptions = PbgTaskStatus::getStatuses(); $statusOptions = PbgTaskStatus::getStatuses();
@@ -60,10 +151,10 @@ class QuickSearchController extends Controller
return view("quick-search.detail", compact("data", 'statusOptions', 'applicationTypes')); return view("quick-search.detail", compact("data", 'statusOptions', 'applicationTypes'));
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { } 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.'); return redirect()->route('quick-search.index')->with('error', 'Data tidak ditemukan.');
} catch (\Throwable $e) { } 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` return response()->view('pages.404', [], 500); // Optional: create `resources/views/errors/500.blade.php`
} }
} }

View File

@@ -56,10 +56,24 @@ class PbgTaskController extends Controller
*/ */
public function show(string $id) 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(); $statusOptions = PbgTaskStatus::getStatuses();
$applicationTypes = PbgTaskApplicationTypes::labels(); $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
View File

View File

@@ -3,17 +3,12 @@
namespace App\Http\Controllers\Settings; namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Services\ServiceSIMBG;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Exception; use Exception;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
class SyncronizeController extends Controller class SyncronizeController extends Controller
{ {
protected $service_simbg;
public function __construct(ServiceSIMBG $service_simbg){
$this->service_simbg = $service_simbg;
}
public function index(Request $request){ public function index(Request $request){
$menuId = $request->query('menu_id'); $menuId = $request->query('menu_id');
$user = Auth::user(); $user = Auth::user();
@@ -37,36 +32,4 @@ class SyncronizeController extends Controller
return view('settings.syncronize.index', compact('creator', 'updater', 'destroyer')); return view('settings.syncronize.index', compact('creator', 'updater', 'destroyer'));
} }
public function syncPbgTask(){
$res = $this->service_simbg->syncTaskPBG();
return $res;
}
public function syncronizeTask(Request $request){
$res = $this->service_simbg->syncTaskPBG();
return redirect()->back()->with('success', 'Processing completed successfully');
}
public function getUserToken(){
$res = $this->service_simbg->getToken();
return $res;
}
public function syncIndexIntegration(Request $request, $uuid){
$token = $request->get('token');
$res = $this->service_simbg->syncIndexIntegration($uuid);
return $res;
}
public function syncTaskDetailSubmit(Request $request, $uuid){
$token = $request->get('token');
$res = $this->service_simbg->syncTaskDetailSubmit($uuid, $token);
return $res;
}
public function syncTaskAssignments($uuid){
$res = $this->service_simbg->syncTaskAssignments($uuid);
return $res;
}
} }

View 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)
{
//
}
}

View 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
View File

View File

@@ -22,13 +22,14 @@ class SpatialPlanningRequest extends FormRequest
public function rules(): array public function rules(): array
{ {
return [ return [
'name' => 'string', 'name' => 'nullable|string',
'kbli' => 'string', 'kbli' => 'nullable|string',
'activities' => 'string', 'activities' => 'nullable|string',
'area' => 'string', 'area' => 'nullable|string',
'location' => 'string', 'location' => 'nullable|string',
'number' => 'string', 'number' => 'nullable|string',
'date' => 'date_format:Y-m-d', 'date' => 'nullable|date_format:Y-m-d',
'is_terbit' => 'nullable|boolean',
]; ];
} }

View File

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

View 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'],
];
}
}

View File

@@ -19,8 +19,8 @@ class DataSettingResource extends JsonResource
'key' => $this->key, 'key' => $this->key,
'value' => $this->value, 'value' => $this->value,
'type' => $this->type, 'type' => $this->type,
'created_at' => $this->created_at->toDateTimeString(), 'created_at' => $this->created_at ? $this->created_at->toDateTimeString() : null,
'updated_at' => $this->updated_at->toDateTimeString(), 'updated_at' => $this->updated_at ? $this->updated_at->toDateTimeString() : null,
]; ];
} }
} }

View File

@@ -22,6 +22,11 @@ class MenuResource extends JsonResource
'url' => $this->url, 'url' => $this->url,
'sort_order' => $this->sort_order, 'sort_order' => $this->sort_order,
'parent' => $this->parent ? new MenuResource($this->parent) : null, 'parent' => $this->parent ? new MenuResource($this->parent) : null,
'children' => $this->when($this->relationLoaded('children'), function () {
return $this->children->sortBy('sort_order')->map(function ($child) {
return new MenuResource($child);
});
}),
'created_at' => $this->created_at, 'created_at' => $this->created_at,
'updated_at' => $this->updated_at 'updated_at' => $this->updated_at
]; ];

View File

@@ -42,6 +42,9 @@ class RequestAssignmentResouce extends JsonResource
->where('pbg_type', 'bukti_bayar') ->where('pbg_type', 'bukti_bayar')
->sortByDesc('created_at') ->sortByDesc('created_at')
->first(), ->first(),
'pbg_task_retributions' => $this->pbg_task_retributions,
'pbg_task_detail' => $this->pbg_task_detail,
'pbg_status' => $this->pbg_status,
]; ];
} }
} }

View File

@@ -5,7 +5,7 @@ namespace App\Http\Resources;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
class SpatialPlanningsResource extends JsonResource class TaxationsResource extends JsonResource
{ {
/** /**
* Transform the resource into an array. * Transform the resource into an array.

View 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;
}
}

View File

@@ -8,10 +8,11 @@ use App\Models\ImportDatasource;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable; use Illuminate\Foundation\Queue\Queueable;
use App\Services\ServiceTabPbgTask; use App\Services\ServiceTabPbgTask;
use App\Services\ServiceGoogleSheet;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class RetrySyncronizeJob implements ShouldQueue class RetrySyncronizeJob implements ShouldQueue
{ {
use Queueable, Dispatchable, InteractsWithQueue, SerializesModels; use Queueable, Dispatchable, InteractsWithQueue, SerializesModels;
@@ -28,7 +29,6 @@ class RetrySyncronizeJob implements ShouldQueue
{ {
try{ try{
$service_tab_pbg_task = app(ServiceTabPbgTask::class); $service_tab_pbg_task = app(ServiceTabPbgTask::class);
$service_google_sheet = app(ServiceGoogleSheet::class);
$failed_import = ImportDatasource::find($this->import_datasource_id); $failed_import = ImportDatasource::find($this->import_datasource_id);
@@ -46,10 +46,7 @@ class RetrySyncronizeJob implements ShouldQueue
throw $e; throw $e;
} }
$data_setting_result = $service_google_sheet->get_big_resume_data(); BigdataResume::generateResumeData($failed_import->id, date('Y'), "simbg");
BigdataResume::generateResumeData($failed_import->id, "all", $data_setting_result);
BigdataResume::generateResumeData($failed_import->id, now()->year, $data_setting_result);
$failed_import->update([ $failed_import->update([
'status' => ImportDatasourceStatus::Success->value, 'status' => ImportDatasourceStatus::Success->value,
@@ -58,7 +55,7 @@ class RetrySyncronizeJob implements ShouldQueue
'failed_uuid' => null 'failed_uuid' => null
]); ]);
}catch(\Exception $e){ }catch(\Exception $e){
\Log::error("RetrySyncronizeJob Failed: ". $e->getMessage(), [ Log::error("RetrySyncronizeJob Failed: ". $e->getMessage(), [
'exception' => $e, 'exception' => $e,
]); ]);
if(isset($failed_import)){ if(isset($failed_import)){

View File

@@ -4,6 +4,7 @@ namespace App\Jobs;
use App\Models\BigdataResume; use App\Models\BigdataResume;
use App\Models\ImportDatasource; use App\Models\ImportDatasource;
use App\Models\PbgTask;
use App\Services\ServiceGoogleSheet; use App\Services\ServiceGoogleSheet;
use App\Services\ServicePbgTask; use App\Services\ServicePbgTask;
use App\Services\ServiceTabPbgTask; use App\Services\ServiceTabPbgTask;
@@ -21,72 +22,189 @@ class ScrapingDataJob implements ShouldQueue
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; 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() 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_pbg_task = app(ServicePbgTask::class);
$service_tab_pbg_task = app(ServiceTabPbgTask::class); $service_tab_pbg_task = app(ServiceTabPbgTask::class);
$service_google_sheet = app(ServiceGoogleSheet::class);
$service_token = app(ServiceTokenSIMBG::class); // Create ImportDatasource record
// Create a record with "processing" status
$import_datasource = ImportDatasource::create([ $import_datasource = ImportDatasource::create([
'message' => 'Initiating scraping...', 'message' => 'Starting optimized scraping process...',
'response_body' => null, 'response_body' => null,
'status' => 'processing', 'status' => 'processing',
'start_time' => now(), 'start_time' => now(),
'failed_uuid' => null 'failed_uuid' => null
]); ]);
$import_datasource->update(['message' => 'Scraping PBG Task parent data...']);
$failed_uuid = null;
// Run the scraping services
$service_google_sheet->run_service();
$service_pbg_task->run_service(); $service_pbg_task->run_service();
try{
$service_tab_pbg_task->run_service();
}catch(\Exception $e){
$failed_uuid = $service_tab_pbg_task->getFailedUUID();
throw $e;
}
$data_setting_result = $service_google_sheet->get_big_resume_data(); // STEP 3: Get all PBG tasks for detail scraping
$totalTasks = PbgTask::count();
BigdataResume::generateResumeData($import_datasource->id, "all", $data_setting_result); $import_datasource->update([
BigdataResume::generateResumeData($import_datasource->id, now()->year, $data_setting_result); 'message' => "Scraping details for {$totalTasks} PBG tasks..."
]);
// Update status to success // 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 {
// 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}%)"
]);
}
} catch (\Exception $e) {
Log::warning("Failed to process task details", [
'uuid' => $pbg_task->uuid,
'error' => $e->getMessage()
]);
// Store failed UUID but continue processing
$failed_uuid = $pbg_task->uuid;
// Only stop if it's a critical error
if ($this->isCriticalError($e)) {
throw $e;
}
}
}
});
$import_datasource->update(['message' => 'Scraping Google Sheet data...']);
$service_google_sheet->run_service();
$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([ $import_datasource->update([
'status' => 'success', 'status' => 'success',
'message' => 'Scraping completed successfully.', 'message' => "Scraping completed successfully. Processed {$processedTasks}/{$totalTasks} tasks.",
'finish_time' => now() '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) { } 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 // Update ImportDatasource with failure info
if (isset($import_datasource)) { if ($import_datasource) {
$import_datasource->update([ $import_datasource->update([
'status' => 'failed', 'status' => 'failed',
'response_body' => 'Error: ' . $e->getMessage(), 'message' => "Scraping failed: {$e->getMessage()}. Processed {$processedTasks}/{$totalTasks} tasks.",
'response_body' => 'Scraping process interrupted due to error',
'finish_time' => now(), 'finish_time' => now(),
'failed_uuid' => $failed_uuid, 'failed_uuid' => $failed_uuid,
]); ]);
} }
// Mark the job as failed // Don't retry this job
$this->fail($e); $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;
}
} }

View File

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

View File

@@ -4,6 +4,8 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use App\Enums\PbgTaskStatus;
use App\Services\ServiceGoogleSheet;
class BigdataResume extends Model class BigdataResume extends Model
{ {
@@ -30,6 +32,12 @@ class BigdataResume extends Model
'issuance_realization_pbg_sum', 'issuance_realization_pbg_sum',
'process_in_technical_office_count', 'process_in_technical_office_count',
'process_in_technical_office_sum', '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() public function importDatasource()
@@ -37,101 +45,336 @@ class BigdataResume extends Model
return $this->belongsTo(ImportDatasource::class, 'import_datasource_id'); return $this->belongsTo(ImportDatasource::class, 'import_datasource_id');
} }
public static function generateResumeData($import_datasource_id, $year, $data_setting){ public static function generateResumeData($import_datasource_id, $year, $resume_type){
$stats = PbgTask::with(['googleSheet', 'pbg_task_retributions']) // Get accurate counts without joins to avoid duplicates from multiple retributions
->leftJoin('pbg_task_retributions as ptr', 'pbg_task.uuid', '=', 'ptr.pbg_task_uid') // Filter only valid data (is_valid = true)
->leftJoin('pbg_task_google_sheet as ptgs', 'pbg_task.registration_number', '=', 'ptgs.no_registrasi') $verified_count = PbgTask::whereIn('status', PbgTaskStatus::getVerified())
->when($year !== 'all', function ($query) use ($year) { ->where('is_valid', true)
$query->whereYear('pbg_task.task_created_at', (int) $year); ->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(" ->selectRaw("
COUNT(CASE WHEN LOWER(TRIM(ptgs.status_verifikasi)) = 'selesai verifikasi' THEN 1 END) AS verified_count, 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 LOWER(TRIM(ptgs.status_verifikasi)) = 'selesai verifikasi' THEN ptr.nilai_retribusi_bangunan 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,
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 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 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, 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 (LOWER(TRIM(ptgs.status_verifikasi)) != 'selesai verifikasi' OR ptgs.status_verifikasi IS NULL) 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
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
") ")
->first(); ->first();
// Assign Results \Log::info('Stats calculation result', [
$verified_count = $stats->verified_count ?? 0; 'business_count' => $business_count,
$verified_total = $stats->verified_total ?? 0; 'non_business_count' => $non_business_count,
$non_verified_count = $stats->non_verified_count ?? 0; 'business_total' => $business_total,
$non_verified_total = $stats->non_verified_total ?? 0; 'non_business_total' => $non_business_total,
$business_count = $stats->business_count ?? 0; 'non_verified_total' => $non_verified_total,
$business_total = $stats->business_total ?? 0; 'non_verified_tasks_count' => $stats->non_verified_tasks_count ?? 'NULL',
$non_business_count = $stats->non_business_count ?? 0; 'non_verified_with_retribution_count' => $stats->non_verified_with_retribution_count ?? 'NULL'
$non_business_total = $stats->non_business_total ?? 0; ]);
$query_potention = once(function () use ($year) { $service_google_sheet = app(ServiceGoogleSheet::class);
$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;
return self::create([ return self::create([
'import_datasource_id' => $import_datasource_id, 'import_datasource_id' => $import_datasource_id,
'spatial_count' => $spatial_planning_count, 'spatial_count' => $service_google_sheet->getSpatialPlanningWithCalculationCount() ?? 0,
'spatial_sum' => $spatial_planning_total ?? 0.00, 'spatial_sum' => $service_google_sheet->getSpatialPlanningCalculationSum() ?? 0.00,
'potention_count' => $potention_count ?? 0, 'potention_count' => $potention_count,
'potention_sum' => $potention_total ?? 0.00, 'potention_sum' => ($stats->potention_total ?? 0),
'non_verified_count' => $non_verified_count ?? 0, 'non_verified_count' => $non_verified_count,
'non_verified_sum' => $non_verified_total ?? 0.00, 'non_verified_sum' => $non_verified_total,
'verified_count' => $verified_count ?? 0, 'verified_count' => $verified_count,
'verified_sum' => $verified_total ?? 0.00, 'verified_sum' => $stats->verified_total ?? 0.00,
'business_count' => $business_count ?? 0, 'business_count' => $business_count,
'business_sum' => $business_total ?? 0.00, 'business_sum' => $business_total,
'non_business_count' => $non_business_count ?? 0, 'non_business_count' => $non_business_count,
'non_business_sum' => $non_business_total ?? 0.00, 'non_business_sum' => $non_business_total,
'year' => $year, 'year' => $year,
'waiting_click_dpmptsp_count' => $data_setting['MENUNGGU_KLIK_DPMPTSP_COUNT'] ?? 0, 'waiting_click_dpmptsp_count' => $waiting_click_dpmptsp_count,
'waiting_click_dpmptsp_sum' => $data_setting['MENUNGGU_KLIK_DPMPTSP_SUM'] ?? 0.00, 'waiting_click_dpmptsp_sum' => $stats->waiting_click_dpmptsp_total ?? 0.00,
'issuance_realization_pbg_count' => $data_setting['REALISASI_TERBIT_PBG_COUNT'] ?? 0, 'issuance_realization_pbg_count' => $issuance_realization_pbg_count,
'issuance_realization_pbg_sum' => $data_setting['REALISASI_TERBIT_PBG_SUM'] ?? 0.00, 'issuance_realization_pbg_sum' => $stats->issuance_realization_pbg_total ?? 0.00,
'process_in_technical_office_count' => $data_setting['PROSES_DINAS_TEKNIS_COUNT'] ?? 0, 'process_in_technical_office_count' => $process_in_technical_office_count,
'process_in_technical_office_sum' => $data_setting['PROSES_DINAS_TEKNIS_SUM'] ??0.00, '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
View 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'])
];
}
}

View 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);
}
}

View 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
View 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,
]
);
}
}

View File

@@ -27,7 +27,8 @@ class PbgTask extends Model
'consultation_type', 'consultation_type',
'due_date', 'due_date',
'land_certificate_phase', 'land_certificate_phase',
'task_created_at' 'task_created_at',
'is_valid'
]; ];
public function pbg_task_retributions(){ public function pbg_task_retributions(){
@@ -38,8 +39,12 @@ class PbgTask extends Model
return $this->hasOne(PbgTaskIndexIntegrations::class, 'pbg_task_uid', 'uuid'); 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(){ 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() public function taskAssignments()
@@ -50,4 +55,91 @@ class PbgTask extends Model
public function attachments(){ public function attachments(){
return $this->hasMany(PbgTaskAttachment::class, 'pbg_task_id', 'id'); 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');
}
} }

View 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
);
}
}

View 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;
}
}

View 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;
}
}
}

View 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 ?? [];
}
}

View 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]
);
}
}

View 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
];
}
}

View File

@@ -2,8 +2,10 @@
namespace App\Models; namespace App\Models;
use App\Traits\HasRetributionCalculation;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
/** /**
* Class SpatialPlanning * Class SpatialPlanning
* *
@@ -23,6 +25,7 @@ use Illuminate\Database\Eloquent\Model;
*/ */
class SpatialPlanning extends Model class SpatialPlanning extends Model
{ {
use HasRetributionCalculation;
protected $perPage = 20; protected $perPage = 20;
@@ -31,7 +34,209 @@ class SpatialPlanning extends Model
* *
* @var array<int, string> * @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
View 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
View File

5
app/Providers/AppServiceProvider.php Executable file → Normal file
View File

@@ -14,8 +14,6 @@ use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\View; use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Carbon\Carbon; use Carbon\Carbon;
use App\Services\ServiceSIMBG;
use App\Services\GoogleSheetService;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
@@ -64,7 +62,8 @@ class AppServiceProvider extends ServiceProvider
$query->whereHas('roles', function ($subQuery) use ($user) { $query->whereHas('roles', function ($subQuery) use ($user) {
$subQuery->whereIn('roles.id', $user->roles->pluck('id')) $subQuery->whereIn('roles.id', $user->roles->pluck('id'))
->where('role_menu.allow_show', 1); ->where('role_menu.allow_show', 1);
}); })
->orderBy('sort_order', 'asc');
}]) }])
->whereNull('parent_id') // Ambil hanya menu utama ->whereNull('parent_id') // Ambil hanya menu utama
->orderBy('sort_order', 'asc') ->orderBy('sort_order', 'asc')

0
app/Providers/AuthServiceProvider.php Executable file → Normal file
View File

0
app/Providers/BroadcastServiceProvider.php Executable file → Normal file
View File

0
app/Providers/EventServiceProvider.php Executable file → Normal file
View File

3
app/Providers/RouteServiceProvider.php Executable file → Normal file
View File

@@ -7,6 +7,7 @@ use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvi
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use App\Models\Menu;
class RouteServiceProvider extends ServiceProvider class RouteServiceProvider extends ServiceProvider
{ {
@@ -17,7 +18,7 @@ class RouteServiceProvider extends ServiceProvider
* *
* @var string * @var string
*/ */
public const HOME = '/home'; public const HOME = '/dashboards/bigdata';
/** /**
* Define your route model bindings, pattern filters, and other route configuration. * Define your route model bindings, pattern filters, and other route configuration.

View File

@@ -1,153 +0,0 @@
<?php
namespace App\Services;
use Google_Client;
use Google_Service_Sheets;
class GoogleSheetService
{
/**
* Create a new class instance.
*/
protected $client;
protected $service;
protected $spreadsheetID;
protected $service_sheets;
public function __construct()
{
$this->client = new Google_Client();
$this->client->setApplicationName("Sibedas Google Sheets API");
$this->client->setScopes([Google_Service_Sheets::SPREADSHEETS_READONLY]);
$this->client->setAuthConfig(storage_path("app/teak-banner-450003-s8-ea05661d9db0.json"));
$this->client->setAccessType("offline");
$this->service = new Google_Service_Sheets($this->client);
$this->spreadsheetID = env("SPREAD_SHEET_ID");
$this->service_sheets = new Google_Service_Sheets($this->client);
}
public function getSheetData($range){
$response = $this->service->spreadsheets_values->get($this->spreadsheetID, $range);
return $response->getValues();
}
public function getLastRowByColumn($column = "A")
{
try{
// Ambil spreadsheet
$spreadsheet = $this->service->spreadsheets->get($this->spreadsheetID);
$sheets = $spreadsheet->getSheets();
if (!empty($sheets)) {
// Ambil nama sheet pertama dengan benar
$firstSheetTitle = $sheets[0]->getProperties()->getTitle();
// ✅ Format range harus benar!
$range = "{$firstSheetTitle}!{$column}:{$column}";
// Ambil data dari kolom yang diminta
$response = $this->service->spreadsheets_values->get($this->spreadsheetID, $range);
$values = $response->getValues();
// Cek nilai terakhir yang tidak kosong
$lastRow = 0;
if (!empty($values)) {
foreach ($values as $index => $row) {
if (!empty($row[0])) { // Jika ada data, update lastRow
$lastRow = $index + 1;
}
}
}
return $lastRow;
}
return 0;
}catch(\Exception $e){
throw $e;
}
}
public function getHeader()
{
try{
$spreadsheet = $this->service->spreadsheets->get($this->spreadsheetID);
$sheets = $spreadsheet->getSheets();
// Ambil nama sheet pertama
$firstSheetTitle = $sheets[0]->getProperties()->getTitle();
// Ambil data dari baris pertama (header)
$range = "{$firstSheetTitle}!1:1";
$response = $this->service->spreadsheets_values->get($this->spreadsheetID, $range);
$values = $response->getValues();
// Kembalikan header (baris pertama)
return !empty($values) ? $values[0] : [];
}catch(\Exception $e){
throw $e;
}
}
public function getLastColumn()
{
$spreadsheet = $this->service->spreadsheets->get($this->spreadsheetID);
$sheets = $spreadsheet->getSheets();
// Ambil nama sheet pertama
$firstSheetTitle = $sheets[0]->getProperties()->getTitle();
// Ambil baris pertama untuk mendapatkan jumlah kolom yang terisi
$range = "{$firstSheetTitle}!1:1";
$response = $this->service->spreadsheets_values->get($this->spreadsheetID, $range);
$values = $response->getValues();
// Hitung jumlah kolom yang memiliki nilai
return !empty($values) ? count(array_filter($values[0], fn($value) => $value !== "")) : 0;
}
public function getSheetDataCollection($totalRow = 10){
try{
$spreadsheet = $this->service->spreadsheets->get($this->spreadsheetID);
$sheets = $spreadsheet->getSheets();
$firstSheetTitle = $sheets[0]->getProperties()->getTitle();
$header = $this->getHeader();
$header = array_map(function($columnHeader) {
// Trim spaces first, then replace non-alphanumeric characters with underscores
$columnHeader = trim($columnHeader);
return strtolower(preg_replace('/[^A-Za-z0-9_]/', '_', $columnHeader));
}, $header);
$range = "{$firstSheetTitle}!2:{$totalRow}";
$response = $this->service->spreadsheets_values->get($this->spreadsheetID, $range);
$values = $response->getValues();
$mappedData = [];
if (!empty($values)) {
foreach ($values as $row) {
$rowData = [];
foreach ($header as $index => $columnHeader) {
// Map header to the corresponding value from the row
$rowData[$columnHeader] = isset($row[$index]) ? $row[$index] : null;
}
$mappedData[] = $rowData;
}
}
return $mappedData;
}catch(\Exception $e){
throw $e;
}
}
public function get_data_by_sheet($no_sheet = 1){
$spreadsheet = $this->service->spreadsheets->get($this->spreadsheetID);
$sheets = $spreadsheet->getSheets();
$sheetTitle = $sheets[$no_sheet]->getProperties()->getTitle();
$range = "{$sheetTitle}";
$response = $this->service->spreadsheets_values->get($this->spreadsheetID, $range);
$values = $response->getValues();
return!empty($values)? $values : [];
}
}

View 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();
}
}

View File

@@ -1,99 +0,0 @@
<?php
namespace App\Services;
use App\Traits\GlobalApiResponse;
use GuzzleHttp\Client;
use Exception;
class ServiceClient
{
use GlobalApiResponse;
private $client;
private $baseUrl;
private $headers;
/**
* Create a new class instance.
*/
public function __construct($baseUrl = '', $headers = [])
{
$this->client = new Client();
$this->baseUrl = $baseUrl;
$this->headers = array_merge(
[
'Accept' => 'application/json',
'Content-Type' => 'application/json'
],
$headers
);
}
public function makeRequest($url, $method = 'GET', $body = null, $headers = [], $timeout = 14400){
try {
$headers = array_merge($this->headers, $headers);
$options = [
'headers' => $headers,
'timeout' => $timeout,
'connect_timeout' => 60
];
if ($body) {
$options['json'] = $body; // Guzzle akan mengonversi array ke JSON
}
$response = $this->client->request($method, $this->baseUrl . $url, $options);
$responseBody = (string) $response->getBody();
if (!str_contains($response->getHeaderLine('Content-Type'), 'application/json')) {
\Log::error('Unexpected response format: ' . $responseBody);
return $this->resError('API response is not JSON');
}
$resultResponse = json_decode($responseBody, true, 512, JSON_THROW_ON_ERROR);
return $this->resSuccess($resultResponse);
} catch (\GuzzleHttp\Exception\ClientException $e) {
// Handle 4xx errors (e.g., 401 Unauthorized)
$responseBody = (string) $e->getResponse()->getBody();
$errorResponse = json_decode($responseBody, true);
if (isset($errorResponse['code']) && $errorResponse['code'] === 'token_not_valid') {
return $this->resError('Invalid token, please refresh your token.', $errorResponse, 401);
}
return $this->resError('Client error from API', $errorResponse, $e->getResponse()->getStatusCode());
} catch (\GuzzleHttp\Exception\ServerException $e) {
// Handle 5xx errors (e.g., Internal Server Error)
return $this->resError('Server error from API', (string) $e->getResponse()->getBody(), 500);
} catch (\GuzzleHttp\Exception\RequestException $e) {
// Handle network errors (e.g., timeout, connection issues)
return $this->resError('Network error: ' . $e->getMessage(), null, 503);
} catch (Exception $e) {
// Handle unexpected errors
return $this->resError('Unexpected error: ' . $e->getMessage(), null, 500);
}
}
// Fungsi untuk melakukan permintaan GET
public function get($url, $headers = [])
{
return $this->makeRequest($url, 'GET', null, $headers);
}
// Fungsi untuk melakukan permintaan POST
public function post($url, $body, $headers = [])
{
return $this->makeRequest($url, 'POST', $body, $headers);
}
// Fungsi untuk melakukan permintaan PUT
public function put($url, $body, $headers = [])
{
return $this->makeRequest($url, 'PUT', $body, $headers);
}
// Fungsi untuk melakukan permintaan DELETE
public function delete($url, $headers = [])
{
return $this->makeRequest($url, 'DELETE', null, $headers);
}
}

View File

@@ -1,14 +1,21 @@
<?php <?php
namespace App\Services; namespace App\Services;
use App\Models\BigdataResume;
use App\Models\DataSetting; use App\Models\DataSetting;
use App\Models\ImportDatasource; use App\Models\ImportDatasource;
use App\Models\PbgTaskGoogleSheet; use App\Models\PbgTaskGoogleSheet;
use App\Models\SpatialPlanning;
use App\Models\RetributionCalculation;
use Carbon\Carbon; use Carbon\Carbon;
use Exception; use Exception;
use Google_Client; use Google\Client as Google_Client;
use Google_Service_Sheets; use Google\Service\Sheets as Google_Service_Sheets;
use Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use App\Models\PbgTask;
class ServiceGoogleSheet class ServiceGoogleSheet
{ {
protected $client; protected $client;
@@ -32,8 +39,8 @@ class ServiceGoogleSheet
public function run_service(){ public function run_service(){
try{ try{
$this->sync_big_data();
$this->sync_google_sheet_data(); $this->sync_google_sheet_data();
$this->sync_pbg_task_payments();
}catch(Exception $e){ }catch(Exception $e){
throw $e; throw $e;
} }
@@ -107,7 +114,7 @@ class ServiceGoogleSheet
'tanggal_skrd' => $this->convertToDate($cleanValue($row[33] ?? null)), 'tanggal_skrd' => $this->convertToDate($cleanValue($row[33] ?? null)),
'ptsp' => $cleanValue($row[34] ?? null), 'ptsp' => $cleanValue($row[34] ?? null),
'selesai_terbit' => $cleanValue($row[35] ?? null), 'selesai_terbit' => $cleanValue($row[35] ?? null),
'tanggal_pembayaran' => $cleanValue($row[36] ?? null), 'tanggal_pembayaran' => $this->convertToDate($cleanValue($row[36] ?? null)),
'format_sts' => $cleanValue($row[37] ?? null), 'format_sts' => $cleanValue($row[37] ?? null),
'tahun_terbit' => (int) $cleanValue($row[38] ?? null), 'tahun_terbit' => (int) $cleanValue($row[38] ?? null),
'tahun_berjalan' => (int) $cleanValue($row[39] ?? null), 'tahun_berjalan' => (int) $cleanValue($row[39] ?? null),
@@ -131,7 +138,16 @@ class ServiceGoogleSheet
} }
// Count occurrences of each no_registrasi // 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) // Filter duplicates (those appearing more than once)
$duplicates = array_filter($registrasiCounts, function ($count) { $duplicates = array_filter($registrasiCounts, function ($count) {
@@ -142,8 +158,14 @@ class ServiceGoogleSheet
Log::warning("Duplicate no_registrasi found", ['duplicates' => array_keys($duplicates)]); Log::warning("Duplicate no_registrasi found", ['duplicates' => array_keys($duplicates)]);
} }
// Remove duplicates before upsert // Remove duplicates before upsert - filter out entries with null no_registrasi
$mapUpsert = collect($mapUpsert)->unique('no_registrasi')->values()->all(); $mapUpsert = collect($mapUpsert)
->filter(function($item) {
return !empty($item['no_registrasi']);
})
->unique('no_registrasi')
->values()
->all();
$batchSize = 1000; $batchSize = 1000;
$chunks = array_chunk($mapUpsert, $batchSize); $chunks = array_chunk($mapUpsert, $batchSize);
@@ -196,9 +218,19 @@ class ServiceGoogleSheet
} }
foreach ($data_setting_result as $key => $value) { 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 ["key" => $key], // Find by key
["value" => $value] // Update or insert value ["value" => $processedValue] // Update or insert value
); );
} }
@@ -210,48 +242,619 @@ class ServiceGoogleSheet
} }
} }
public function get_big_resume_data(){ public function sync_leader_data(){
$import_datasource = ImportDatasource::create([
'message' => 'Processing leader data',
'status' => 'processing',
'start_time' => now(),
'failed_uuid' => null
]);
try { try {
$sheet_big_data = $this->get_data_by_sheet(); $sections = [
$data_setting_result = []; // Initialize result storage '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) { foreach ($sections as $key => $identifier) {
// Check for section headers $values = $this->get_values_from_section($identifier, [10, 11], 9);
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";
}
// If a section is found and we reach "Grand Total", save the corresponding values if (!empty($values)) {
if ($found_section && isset($row[0]) && trim($row[0]) === "Grand Total") { $result[$key] = [
if ($found_section === "MENUNGGU_KLIK_DPMPTSP") { 'identifier' => $identifier,
$data_setting_result["MENUNGGU_KLIK_DPMPTSP_COUNT"] = $this->convertToInteger($row[2]) ?? null; 'total' => $values[0] ?? null, // index 0 untuk total/jumlah
$data_setting_result["MENUNGGU_KLIK_DPMPTSP_SUM"] = $this->convertToDecimal($row[3]) ?? null; 'nominal' => $values[1] ?? null // index 1 untuk nominal
} 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;
} }
} }
return $data_setting_result;
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 {
$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
];
}
}
// 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){ }catch(Exception $exception){
Log::error("Error getting big resume data", ['error' => $exception->getMessage()]); Log::error("Error getting big resume data", ['error' => $exception->getMessage()]);
throw $exception; 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 = [];
$inserted = 0;
// Stream rows in chunks from API to avoid loading full sheet
$rowStart = 2; // data starts from row 2
$chunkRowSize = 1000; // 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
}
Log::info('Chunk fetched', [
'rowStart' => $rowStart,
'rowEnd' => $rowEnd,
'count' => count($values)
]);
// 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(); }
}
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); $spreadsheet = $this->service->spreadsheets->get($this->spreadsheetID);
$sheets = $spreadsheet->getSheets(); $sheets = $spreadsheet->getSheets();
$sheetTitle = $sheets[$no_sheet]->getProperties()->getTitle(); $sheetTitle = $sheets[$no_sheet]->getProperties()->getTitle();
@@ -261,9 +864,110 @@ class ServiceGoogleSheet
return!empty($values)? $values : []; 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) { private function convertToInteger($value) {
// Check if the value is an empty string, and return null if true // Check if the value is null or empty string, and return null if true
if (trim($value) === "") { if ($value === null || trim($value) === "") {
return null; return null;
} }
@@ -300,6 +1004,54 @@ class ServiceGoogleSheet
return is_numeric($value) ? (float) number_format((float) $value, 2, '.', '') : null; 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) private function convertToDate($dateString)
{ {
try { try {

View File

@@ -106,7 +106,7 @@ class ServicePbgTask
}; };
do { 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); $fetch_data = $fetchData($url);
if (!$fetch_data) { if (!$fetch_data) {
@@ -123,6 +123,8 @@ class ServicePbgTask
$data = $response['data']; $data = $response['data'];
$totalPage = isset($response['total_page']) ? (int) $response['total_page'] : 1; $totalPage = isset($response['total_page']) ? (int) $response['total_page'] : 1;
Log::info("Total data scraping {$totalPage}");
$saved_data = []; $saved_data = [];
foreach ($data as $item) { foreach ($data as $item) {
$saved_data[] = [ $saved_data[] = [

View File

@@ -1,663 +0,0 @@
<?php
namespace App\Services;
use App\Enums\ImportDatasourceStatus;
use App\Models\BigdataResume;
use App\Models\GlobalSetting;
use App\Models\ImportDatasource;
use App\Models\PbgTaskIndexIntegrations;
use App\Models\PbgTaskPrasarana;
use App\Models\PbgTaskRetributions;
use App\Models\TaskAssignment;
use Exception;
use App\Models\PbgTask;
use App\Traits\GlobalApiResponse;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
use App\Services\ServiceClient;
use App\Services\GoogleSheetService;
use App\Models\DataSetting;
use App\Models\PbgTaskGoogleSheet;
class ServiceSIMBG
{
use GlobalApiResponse;
private $email;
private $password;
private $simbg_host;
private $fetch_per_page;
private $service_client;
private $googleSheetService;
/**
* Create a new class instance.
*/
public function __construct(GoogleSheetService $googleSheetService)
{
$settings = GlobalSetting::whereIn('key', [
'SIMBG_EMAIL', 'SIMBG_PASSWORD', 'SIMBG_HOST', 'FETCH_PER_PAGE'
])->pluck('value', 'key');
$this->email = trim((string) ($settings['SIMBG_EMAIL'] ?? ""));
$this->password = trim((string) ($settings['SIMBG_PASSWORD'] ?? ""));
$this->simbg_host = trim((string) ($settings['SIMBG_HOST'] ?? ""));
$this->fetch_per_page = trim((string) ($settings['FETCH_PER_PAGE'] ?? ""));
$this->service_client = new ServiceClient($this->simbg_host);
$this->googleSheetService = $googleSheetService;
}
public function getToken(){
try{
$url = "/api/user/v1/auth/login/";
$body = [
'email' => $this->email,
'password' => $this->password,
];
$res = $this->service_client->post($url, $body);
if(!$res->original['success']){
Log::error("Token not retrieved ", ['response' => $res]);
throw new Exception("Token not retrieved.");
}
return $res;
}catch(Exception $e){
Log::error("Error on method get token ", ['response' => $e->getMessage()]);
throw $e;
}
}
public function syncIndexIntegration($uuids)
{
try{
if(empty($uuids)){
return false;
}
$initResToken = $this->getToken();
if (empty($initResToken->original['data']['token']['access'])) {
Log::error("API response indicates failure", ['token' => 'Failed to retrieve token']);
return false;
}
$token = $initResToken->original['data']['token']['access'];
$integrations = [];
foreach($uuids as $uuid){
$url = "/api/pbg/v1/detail/" . $uuid . "/retribution/indeks-terintegrasi/";
$headers = [
'Authorization' => "Bearer " . $token,
];
$res = $this->service_client->get($url, $headers);
if (empty($res->original['success']) || !$res->original['success']) {
// Log error
Log::error("API response indicates failure", ['url' => $url, 'uuid' => $uuid]);
continue;
}
$data = $res->original['data']['data'] ?? null;
if (!$data) {
Log::error("No valid data returned from API", ['url' => $url, 'uuid' => $uuid]);
continue;
}
$integrations[] = [
'pbg_task_uid' => $uuid,
'indeks_fungsi_bangunan' => $data['indeks_fungsi_bangunan'] ?? null,
'indeks_parameter_kompleksitas' => $data['indeks_parameter_kompleksitas'] ?? null,
'indeks_parameter_permanensi' => $data['indeks_parameter_permanensi'] ?? null,
'indeks_parameter_ketinggian' => $data['indeks_parameter_ketinggian'] ?? null,
'faktor_kepemilikan' => $data['faktor_kepemilikan'] ?? null,
'indeks_terintegrasi' => $data['indeks_terintegrasi'] ?? null,
'total' => $data['total'] ?? null,
];
}
PbgTaskIndexIntegrations::upsert($integrations, ['pbg_task_uid'], ['indeks_fungsi_bangunan',
'indeks_parameter_kompleksitas', 'indeks_parameter_permanensi', 'indeks_parameter_ketinggian', 'faktor_kepemilikan', 'indeks_terintegrasi', 'total']);
return true;
}catch (Exception $e){
Log::error('error when sync index integration ', ['index integration'=> $e->getMessage()]);
throw $e;
}
}
public function syncTaskPBG()
{
try {
Log::info("Processing google sheet sync");
$importDatasource = ImportDatasource::create([
'status' => ImportDatasourceStatus::Processing->value,
]);
// sync google sheet first
$totalRowCount = $this->googleSheetService->getLastRowByColumn("C");
$sheetData = $this->googleSheetService->getSheetDataCollection($totalRowCount);
$sheet_big_data = $this->googleSheetService->get_data_by_sheet();
$data_setting_result = []; // Initialize result storage
$found_section = null; // Track which section is found
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";
}
// 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;
}
}
Log::info("data setting result", ['result' => $data_setting_result]);
foreach ($data_setting_result as $key => $value) {
DataSetting::updateOrInsert(
["key" => $key], // Find by key
["value" => $value] // Update or insert value
);
}
$mapToUpsert = [];
foreach ($sheetData as $data) {
$mapToUpsert[] = [
'no_registrasi' => $this->cleanString($data['no__registrasi'] ?? null),
'jenis_konsultasi' => $this->cleanString($data['jenis_konsultasi'] ?? null),
'fungsi_bg' => $this->cleanString($data['fungsi_bg'] ?? null),
'tgl_permohonan' => $this->convertToDate($this->cleanString($data['tgl_permohonan'] ?? null)),
'status_verifikasi' => $this->cleanString($data['status_verifikasi'] ?? null),
'status_permohonan' => $this->convertToDate($this->cleanString($data['status_permohonan'] ?? null)),
'alamat_pemilik' => $this->cleanString($data['alamat_pemilik'] ?? null),
'no_hp' => $this->cleanString($data['no__hp'] ?? null),
'email' => $this->cleanString($data['e_mail'] ?? null),
'tanggal_catatan' => $this->convertToDate($this->cleanString($data['tanggal_catatan'] ?? null)),
'catatan_kekurangan_dokumen' => $this->cleanString($data['catatan_kekurangan_dokumen'] ?? null),
'gambar' => $this->cleanString($data['gambar'] ?? null),
'krk_kkpr' => $this->cleanString($data['krk_kkpr'] ?? null),
'no_krk' => $this->cleanString($data['no__krk'] ?? null),
'lh' => $this->cleanString($data['lh'] ?? null),
'ska' => $this->cleanString($data['ska'] ?? null),
'keterangan' => $this->cleanString($data['keterangan'] ?? null),
'helpdesk' => $this->cleanString($data['helpdesk'] ?? null),
'pj' => $this->cleanString($data['pj'] ?? null),
'kepemilikan' => $this->cleanString($data['kepemilikan'] ?? null),
'potensi_taru' => $this->cleanString($data['potensi_taru'] ?? null),
'validasi_dinas' => $this->cleanString($data['validasi_dinas'] ?? null),
'kategori_retribusi' => $this->cleanString($data['kategori_retribusi'] ?? null),
'no_urut_ba_tpt' => $this->cleanString($data['no__urut_ba_tpt__2024_0001_'] ?? null),
'tanggal_ba_tpt' => $this->convertToDate($this->cleanString($data['tanggal_ba_tpt'] ?? null)),
'no_urut_ba_tpa' => $this->cleanString($data['no__urut_ba_tpa'] ?? null),
'tanggal_ba_tpa' => $this->convertToDate($this->cleanString($data['tanggal_ba_tpa'] ?? null)),
'no_urut_skrd' => $this->cleanString($data['no__urut_skrd__2024_0001_'] ?? null),
'tanggal_skrd' => $this->convertToDate($this->cleanString($data['tanggal_skrd'] ?? null)),
'ptsp' => $this->cleanString($data['ptsp'] ?? null),
'selesai_terbit' => $this->cleanString($data['selesai_terbit'] ?? null),
'tanggal_pembayaran' => $this->convertToDate($this->cleanString($data['tanggal_pembayaran__yyyy_mm_dd_'] ?? null)),
'format_sts' => $this->cleanString($data['format_sts'] ?? null),
'tahun_terbit' => (int) ($data['tahun_terbit'] ?? null),
'tahun_berjalan' => (int) ($data['tahun_berjalan'] ?? null),
'kelurahan' => $this->cleanString($data['kelurahan'] ?? null),
'kecamatan' => $this->cleanString($data['kecamatan'] ?? null),
'lb' => $this->convertToDecimal($data['lb'] ?? null),
'tb' => $this->convertToDecimal($data['tb'] ?? null),
'jlb' => (int) ($data['jlb'] ?? null),
'unit' => (int) ($data['unit'] ?? null),
'usulan_retribusi' => (int) ($data['usulan_retribusi'] ?? null),
'nilai_retribusi_keseluruhan_simbg' => $this->convertToDecimal($data['nilai_retribusi_keseluruhan__simbg_'] ?? null),
'nilai_retribusi_keseluruhan_pad' => $this->convertToDecimal($data['nilai_retribusi_keseluruhan__pad_'] ?? null),
'denda' => $this->convertToDecimal($data['denda'] ?? null),
'latitude' => $this->cleanString($data['latitude'] ?? null),
'longitude' => $this->cleanString($data['longitude'] ?? null),
'nik_nib' => $this->cleanString($data['nik_nib'] ?? null),
'dok_tanah' => $this->cleanString($data['dok__tanah'] ?? null),
'temuan' => $this->cleanString($data['temuan'] ?? null),
];
}
$batchSize = 1000;
$chunks = array_chunk($mapToUpsert, $batchSize);
foreach($chunks as $chunk){
PbgTaskGoogleSheet::upsert($chunk, ["no_registrasi"],[
'jenis_konsultasi',
'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',
'krk_kkpr',
'no_krk',
'lh',
'ska',
'keterangan',
'helpdesk',
'pj',
'kepemilikan',
'potensi_taru',
'validasi_dinas',
'kategori_retribusi',
'no_urut_ba_tpt',
'tanggal_ba_tpt',
'no_urut_ba_tpa',
'tanggal_ba_tpa',
'no_urut_skrd',
'tanggal_skrd',
'ptsp',
'selesai_terbit',
'tanggal_pembayaran',
'format_sts',
'tahun_terbit',
'tahun_berjalan',
'kelurahan',
'kecamatan',
'lb',
'tb',
'jlb',
'unit',
'usulan_retribusi',
'nilai_retribusi_keseluruhan_simbg',
'nilai_retribusi_keseluruhan_pad',
'denda',
'latitude',
'longitude',
'nik_nib',
'dok_tanah',
'temuan',
]);
}
$initResToken = $this->getToken();
if (empty($initResToken->original['data']['token']['access'])) {
$importDatasource->update([
'status' => ImportDatasourceStatus::Failed->value,
'response_body' => 'Failed to retrieve token'
]);
return $this->resError("Failed to retrieve token");
}
$apiToken = $initResToken->original['data']['token']['access'];
$headers = ['Authorization' => "Bearer " . $apiToken];
$url = "/api/pbg/v1/list/?page=1&size={$this->fetch_per_page}&sort=ASC";
$initialResponse = $this->service_client->get($url, $headers);
$totalPage = $initialResponse->original['data']['total_page'] ?? 0;
if ($totalPage == 0) {
$importDatasource->update([
'status' => ImportDatasourceStatus::Failed->value,
'response_body' => 'Invalid response: no total_page'
]);
return $this->resError("Invalid response from API");
}
$savedCount = $failedCount = 0;
Log::info("Fetching tasks", ['total page' => $totalPage]);
for ($currentPage = 1; $currentPage <= $totalPage; $currentPage++) {
try {
$pageUrl = "/api/pbg/v1/list/?page={$currentPage}&size={$this->fetch_per_page}&sort=ASC";
Log::info("Fetching tasks", ['currentPage' => $currentPage]);
$headers = [
'Authorization' => "Bearer " . $apiToken, // Update headers
];
for ($attempt = 0; $attempt < 2; $attempt++) { // Try twice (original + retry)
$response = $this->service_client->get($pageUrl, $headers);
if ($response instanceof \Illuminate\Http\JsonResponse) {
$decodedResponse = json_decode($response->getContent(), true);
if (isset($decodedResponse['errors']['code']) && $decodedResponse['errors']['code'] === 'token_not_valid') {
$initResToken = $this->getToken();
if (!empty($initResToken->original['data']['token']['access'])) {
$new_token = $initResToken->original['data']['token']['access'];
$headers['Authorization'] = "Bearer " . $new_token;
continue;
} else {
Log::error("Failed to refresh token");
return $this->resError("Failed to refresh token");
}
}
}
// Success case, break loop
break;
}
$tasks = $response->original['data']['data'] ?? [];
if (empty($tasks)) {
Log::warning("No data found on page", ['page' => $currentPage]);
continue;
}
$tasksCollective = [];
foreach ($tasks as $item) {
try {
$tasksCollective[] = [
'uuid' => $item['uid'],
'name' => $item['name'],
'owner_name' => $item['owner_name'],
'application_type' => $item['application_type'],
'application_type_name' => $item['application_type_name'],
'condition' => $item['condition'],
'registration_number' => $item['registration_number'],
'document_number' => $item['document_number'],
'address' => $item['address'],
'status' => $item['status'],
'status_name' => $item['status_name'],
'slf_status' => $item['slf_status'] ?? null,
'slf_status_name' => $item['slf_status_name'] ?? null,
'function_type' => $item['function_type'],
'consultation_type' => $item['consultation_type'],
'due_date' => $item['due_date'],
'land_certificate_phase' => $item['land_certificate_phase'],
'task_created_at' => isset($item['created_at']) ? Carbon::parse($item['created_at'])->format('Y-m-d H:i:s') : null,
'updated_at' => now(),
'created_at' => now(),
];
$this->syncTaskDetailSubmit($item['uid'], $apiToken);
$this->syncTaskAssignments($item['uid']);
$savedCount++;
} catch (Exception $e) {
$failedCount++;
Log::error("Failed to process task", [
'error' => $e->getMessage(),
'task' => $item,
]);
continue; // Skip failed task, continue processing the rest
}
}
if (!empty($tasksCollective)) {
PbgTask::upsert($tasksCollective, ['uuid'], [
'name', 'owner_name', 'application_type', 'application_type_name', 'condition',
'registration_number', 'document_number', 'address', 'status', 'status_name',
'slf_status', 'slf_status_name', 'function_type', 'consultation_type', 'due_date',
'land_certificate_phase', 'task_created_at', 'updated_at'
]);
$uuids = array_column($tasksCollective, 'uuid');
$this->syncIndexIntegration($uuids);
}
} catch (Exception $e) {
Log::error("Failed to process page", [
'error' => $e->getMessage(),
'page' => $currentPage,
]);
continue; // Skip the failed page and move to the next
}
}
BigdataResume::generateResumeData($importDatasource->id, "all", $data_setting_result);
BigdataResume::generateResumeData($importDatasource->id, now()->year, $data_setting_result);
// Final update after processing all pages
$importDatasource->update([
'status' => ImportDatasourceStatus::Success->value,
'message' => "Successfully processed: $savedCount, Failed: $failedCount"
]);
Log::info("syncTaskList completed", ['savedCount' => $savedCount, 'failedCount' => $failedCount]);
return $this->resSuccess(['savedCount' => $savedCount, 'failedCount' => $failedCount]);
} catch (Exception $e) {
Log::error("syncTaskList failed", ['error' => $e->getMessage()]);
if (isset($importDatasource)) {
$importDatasource->update([
'status' => ImportDatasourceStatus::Failed->value,
'response_body' => 'Critical failure: ' . $e->getMessage()
]);
}
return $this->resError("Critical failure occurred: " . $e->getMessage());
}
}
public function syncTaskDetailSubmit($uuid, $token)
{
try{
$url = "/api/pbg/v1/detail/" . $uuid . "/retribution/submit/";
$headers = [
'Authorization' => "Bearer " . $token,
];
for ($attempt = 0; $attempt < 2; $attempt++) {
$res = $this->service_client->get($url, $headers);
// Check if response is JsonResponse and decode it
if ($res instanceof \Illuminate\Http\JsonResponse) {
$decodedResponse = json_decode($res->getContent(), true);
if (isset($decodedResponse['errors']['code']) && $decodedResponse['errors']['code'] === 'token_not_valid') {
$initResToken = $this->getToken();
if (!empty($initResToken->original['data']['token']['access'])) {
$new_token = $initResToken->original['data']['token']['access'];
$headers['Authorization'] = "Bearer " . $new_token;
continue;
} else {
Log::error("Failed to refresh token");
return $this->resError("Failed to refresh token");
}
}
}
break;
}
// Ensure response is valid before accessing properties
$responseData = $res->original ?? [];
$data = $responseData['data']['data'] ?? [];
if (empty($data)) {
return false;
}
$detailCreatedAt = isset($data['created_at'])
? Carbon::parse($data['created_at'])->format('Y-m-d H:i:s')
: null;
$detailUpdatedAt = isset($data['updated_at'])
? Carbon::parse($data['updated_at'])->format('Y-m-d H:i:s')
: null;
$pbg_task_retributions = PbgTaskRetributions::updateOrCreate(
['detail_id' => $data['id']],
[
'detail_uid' => $data['uid'] ?? null,
'detail_created_at' => $detailCreatedAt ?? null,
'detail_updated_at' => $detailUpdatedAt ?? null,
'luas_bangunan' => $data['luas_bangunan'] ?? null,
'indeks_lokalitas' => $data['indeks_lokalitas'] ?? null,
'wilayah_shst' => $data['wilayah_shst'] ?? null,
'kegiatan_id' => $data['kegiatan']['id'] ?? null,
'kegiatan_name' => $data['kegiatan']['name'] ?? null,
'nilai_shst' => $data['nilai_shst'] ?? null,
'indeks_terintegrasi' => $data['indeks_terintegrasi'] ?? null,
'indeks_bg_terbangun' => $data['indeks_bg_terbangun'] ?? null,
'nilai_retribusi_bangunan' => $data['nilai_retribusi_bangunan'] ?? null,
'nilai_prasarana' => $data['nilai_prasarana'] ?? null,
'created_by' => $data['created_by'] ?? null,
'pbg_document' => $data['pbg_document'] ?? null,
'underpayment' => $data['underpayment'] ?? null,
'skrd_amount' => $data['skrd_amount'] ?? null,
'pbg_task_uid' => $uuid,
]
);
$pbg_task_retribution_id = $pbg_task_retributions->id;
$prasaranaData = $data['prasarana'] ?? [];
if (!empty($prasaranaData)) {
$insertData = array_map(fn($item) => [
'pbg_task_uid' => $uuid,
'pbg_task_retribution_id' => $pbg_task_retribution_id,
'prasarana_id' => $item['id'] ?? null,
'prasarana_type' => $item['prasarana_type'] ?? null,
'building_type' => $item['building_type'] ?? null,
'total' => $item['total'] ?? null,
'quantity' => $item['quantity'] ?? null,
'unit' => $item['unit'] ?? null,
'index_prasarana' => $item['index_prasarana'] ?? null,
], $prasaranaData);
// Use bulk insert or upsert for faster database operation
PbgTaskPrasarana::upsert($insertData, ['prasarana_id']);
}
return true;
}catch(Exception $e){
Log::error("Failed to sync task detail submit", ['error' => $e->getMessage(), 'uuid' => $uuid]);
throw $e;
}
}
public function syncTaskAssignments($uuid){
try{
$init_token = $this->getToken();
$token = $init_token->original['data']['token']['access'];
$url = "/api/pbg/v1/list-tim-penilai/". $uuid . "/?page=1&size=10";
$headers = [
'Authorization' => "Bearer " . $token,
];
$response = $this->service_client->get($url, $headers);
$datas = $response->original['data']['data'] ?? [];
if(empty($datas)){
return false;
}
$task_assignments = [];
foreach ($datas as $data) {
$task_assignments[] = [
'pbg_task_uid' => $uuid,
'user_id' => $data['user_id'],
'name' => $data['name'],
'username' => $data['username'],
'email' => $data['email'],
'phone_number' => $data['phone_number'],
'role' => $data['role'],
'role_name' => $data['role_name'],
'is_active' => $data['is_active'],
'file' => !empty($data['file']) ? json_encode($data['file']) : null,
'expertise' => !empty($data['expertise']) ? json_encode($data['expertise']) : null,
'experience' => !empty($data['experience']) ? json_encode($data['experience']) : null,
'is_verif' => $data['is_verif'],
'uid' => $data['uid'],
'status' => $data['status'],
'status_name' => $data['status_name'],
'note' => $data['note'],
'ta_id' => $data['id'],
'created_at' => now(),
'updated_at' => now(),
];
}
TaskAssignment::upsert(
$task_assignments,
['uid'],
['ta_id','name', 'username', 'email', 'phone_number', 'role', 'role_name', 'is_active', 'file', 'expertise', 'experience', 'is_verif', 'status', 'status_name', 'note', 'updated_at']
);
return true;
}catch(Exception $e){
Log::error("Failed to sync task assignments", ['error' => $e->getMessage()]);
throw $e;
}
}
protected function convertToDecimal(?string $value): ?float
{
if (empty($value)) {
return null; // Return null if the input is empty
}
// Remove all non-numeric characters except comma and dot
$value = preg_replace('/[^0-9,\.]/', '', $value);
// If the number contains both dot (.) and comma (,)
if (strpos($value, '.') !== false && strpos($value, ',') !== false) {
$value = str_replace('.', '', $value); // Remove thousands separator
$value = str_replace(',', '.', $value); // Convert decimal separator to dot
}
// If only a dot is present (assumed as thousands separator)
elseif (strpos($value, '.') !== false) {
$value = str_replace('.', '', $value); // Remove all dots (treat as thousands separators)
}
// If only a comma is present (assumed as decimal separator)
elseif (strpos($value, ',') !== false) {
$value = str_replace(',', '.', $value); // Convert comma to dot (decimal separator)
}
// Ensure the value is numeric before returning
return is_numeric($value) ? (float) number_format((float) $value, 2, '.', '') : null;
}
protected function convertToInteger($value) {
// Check if the value is an empty string, and return null if true
if (trim($value) === "") {
return null;
}
$cleaned = str_replace('.','', $value);
// Otherwise, cast to integer
return (int) $cleaned;
}
protected function convertToDate($dateString)
{
try {
// Check if the string is empty
if (empty($dateString)) {
return null;
}
// Try to parse the date string
$date = Carbon::parse($dateString);
// Return the Carbon instance
return $date->format('Y-m-d');
} catch (Exception $e) {
// Return null if an error occurs during parsing
return null;
}
}
private function cleanString($value)
{
return isset($value) ? trim(strip_tags($value)) : null;
}
}

View File

@@ -3,7 +3,10 @@
namespace App\Services; namespace App\Services;
use App\Models\GlobalSetting; use App\Models\GlobalSetting;
use App\Models\PbgStatus;
use App\Models\PbgTask; use App\Models\PbgTask;
use App\Models\PbgTaskDetail;
use App\Models\PbgTaskDetailDataList;
use App\Models\PbgTaskIndexIntegrations; use App\Models\PbgTaskIndexIntegrations;
use App\Models\PbgTaskPrasarana; use App\Models\PbgTaskPrasarana;
use App\Models\PbgTaskRetributions; use App\Models\PbgTaskRetributions;
@@ -35,43 +38,259 @@ class ServiceTabPbgTask
$this->user_refresh_token = $auth_data['refresh']; $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 { try {
$pbg_tasks = PbgTask::orderBy('id')->get(); $query = PbgTask::orderBy('id');
$start = false;
foreach ($pbg_tasks as $pbg_task) { // If retry_uuid is provided, start from that UUID
if($retry_uuid){ if ($retry_uuid) {
if($pbg_task->uuid === $retry_uuid){ $retryTask = PbgTask::where('uuid', $retry_uuid)->first();
$start = true; 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; continue;
} }
} }
try{
$this->current_uuid = $pbg_task->uuid; $chunkTime = now()->diffInSeconds($chunkStartTime);
$this->scraping_task_assignments($pbg_task->uuid); Log::info("Processed chunk of {$pbg_tasks->count()} tasks in {$chunkTime} seconds");
$this->scraping_task_retributions($pbg_task->uuid);
$this->scraping_task_integrations($pbg_task->uuid); // Small delay between chunks to prevent API rate limiting
}catch(\Exception $e){ if ($pbg_tasks->count() === $chunk_size) {
Log::error("Failed on UUID: {$this->current_uuid}, Error: " . $e->getMessage()); sleep(1);
throw $e;
} }
} });
Log::info("Successfully completed sync for {$processedCount} PBG Tasks");
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error("Failed to syncronize: " . $e->getMessage()); Log::error("Failed to synchronize: " . $e->getMessage());
throw $e; 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(){ public function getFailedUUID(){
return $this->current_uuid; 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'];
// 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"; $url = "{$this->simbg_host}/api/pbg/v1/list-tim-penilai/{$uuid}/?page=1&size=10";
$options = [ $options = [
@@ -164,7 +383,173 @@ class ServiceTabPbgTask
throw new \Exception("Failed to fetch task assignments for UUID {$uuid} after retries."); 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/"; $url = "{$this->simbg_host}/api/pbg/v1/detail/" . $uuid . "/retribution/submit/";
$options = [ $options = [
@@ -281,7 +666,7 @@ class ServiceTabPbgTask
throw new \Exception("Failed to fetch task retributions for UUID {$uuid} after retries."); 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/"; $url = "{$this->simbg_host}/api/pbg/v1/detail/" . $uuid . "/retribution/indeks-terintegrasi/";
$options = [ $options = [
'headers' => [ 'headers' => [

View 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
View File

@@ -14,6 +14,9 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware) { ->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'validate.api.token.web' => \App\Http\Middleware\ValidateApiTokenForWeb::class,
]);
}) })
->withExceptions(function (Exceptions $exceptions) { ->withExceptions(function (Exceptions $exceptions) {
$exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $th){ $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $th){

0
bootstrap/providers.php Executable file → Normal file
View File

0
composer.json Executable file → Normal file
View File

0
config/app.php Executable file → Normal file
View File

0
config/auth.php Executable file → Normal file
View File

0
config/cache.php Executable file → Normal file
View File

0
config/database.php Executable file → Normal file
View File

0
config/filesystems.php Executable file → Normal file
View File

0
config/logging.php Executable file → Normal file
View File

0
config/mail.php Executable file → Normal file
View File

0
config/queue.php Executable file → Normal file
View File

Some files were not shown because too many files have changed in this diff Show More