Compare commits

...

23 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
56 changed files with 1980 additions and 1719 deletions

View File

@@ -1,123 +0,0 @@
PbgTask::truncate();
exit
(new App\Jobs\ScrapingDataJob())->handle();
(new App\Jobs\ScrapingDataJob())->handle();
(new App\Jobs\ScrapingDataJob())->handle();
exit
(new App\Jobs\ScrapingDataJob())->handle();
exit
(new App\Jobs\ScrapingDataJob())->handle();
(new App\Jobs\ScrapingDataJob())->handle();
exit
exit
BigdataResume::generateResumeData(253,2025,"simbg");
exit
BigdataResume::generateResumeData(253,2025,"simbg");
exit
Bigdataresume::generateResumeData(253,2025,'simbg');
BigdataResume::generateResumeData(253,2025,'simbg');
exit
BigdataResume::generateResumeData(253,2025,"simbg");
BigdataResume::generateResumeData(253,2025,"simbg");
exit
BigdataResume::generateResumeData(253,2025,"simbg");
exit
$importDatasource = \App\Models\ImportDatasource::create([
'message' => 'Testing BigdataResume Generation',
'status' => 'success',
'start_time' => now(),
'finish_time' => now()
]);
$bigdataresume = BigdataResume::generateResumeData($importDatasource->id, 2025, 'simbg');
$bigdataresume = BigdataResume::generateResumeData($importDatasource->id, 2025, 'simbg');
exit
$importDatasource = \App\Models\ImportDatasource::create([
'message' => 'Testing BigdataResume Generation',
'status' => 'success',
'start_time' => now(),
'finish_time' => now()
]);
$bigdataresume = BigdataResume::generateResumeData($importDatasource->id, 2025, 'simbg');
exit
$importDatasource = \App\Models\ImportDatasource::create([
'message' => 'Testing BigdataResume Generation',
'status' => 'success',
'start_time' => now(),
'finish_time' => now()
]);
$bigdataresume = BigdataResume::generateResumeData($importDatasource->id, 2025, 'simbg');
$bigdataresume = BigdataResume::generateResumeData($importDatasource->id, 2025, 'simbg');
exit
$importDatasource = \App\Models\ImportDatasource::create([
'message' => 'Testing BigdataResume Generation',
'status' => 'success',
'start_time' => now(),
'finish_time' => now()
]);
$bigdataresume = BigdataResume::generateResumeData($importDatasource->id, 2025, 'simbg');
exit
$importDatasource = \App\Models\ImportDatasource::create([
'message' => 'Testing BigdataResume Generation',
'status' => 'success',
'start_time' => now(),
'finish_time' => now()
]);
$bigdataresume = BigdataResume::generateResumeData($importDatasource->id, 2025, 'simbg');
exit
$importDatasource = \App\Models\ImportDatasource::create([
'message' => 'Testing BigdataResume Generation',
'status' => 'success',
'start_time' => now(),
'finish_time' => now()
]);
$bigdataresume = BigdataResume::generateResumeData($importDatasource->id, 2025, 'simbg');
$bigdataresume = BigdataResume::generateResumeData($importDatasource->id, 2025, 'simbg');
exit
$importDatasource = \App\Models\ImportDatasource::create([
'message' => 'Testing BigdataResume Generation',
'status' => 'success',
'start_time' => now(),
'finish_time' => now()
]);
$bigdataresume = BigdataResume::generateResumeData($importDatasource->id, 2025, 'simbg');
exit
$importDatasource = \App\Models\ImportDatasource::create([
'message' => 'Testing BigdataResume Generation',
'status' => 'success',
'start_time' => now(),
'finish_time' => now()
]);
$bigdataresume = BigdataResume::generateResumeData($importDatasource->id, 2025, 'simbg');
exit
$importDatasource = \App\Models\ImportDatasource::create([
'message' => 'Testing BigdataResume Generation',
'status' => 'success',
'start_time' => now(),
'finish_time' => now()
]);
$bigdataresume = BigdataResume::generateResumeData($importDatasource->id, 2025, 'simbg');
exit
$importDatasource = \App\Models\ImportDatasource::create([
'message' => 'Testing BigdataResume Generation',
'status' => 'success',
'start_time' => now(),
'finish_time' => now()
]);
$bigdataresume = BigdataResume::generateResumeData($importDatasource->id, 2025, 'simbg');
exit
$importDatasource = \App\Models\ImportDatasource::create([
'message' => 'Testing BigdataResume Generation',
'status' => 'success',
'start_time' => now(),
'finish_time' => now()
]);
$bigdataresume = BigdataResume::generateResumeData($importDatasource->id, 2025, 'simbg');
exit
$importDatasource = \App\Models\ImportDatasource::create([
'message' => 'Testing BigdataResume Generation',
'status' => 'success',
'start_time' => now(),
'finish_time' => now()
]);
$bigdataresume = BigdataResume::generateResumeData($importDatasource->id, 2025, 'simbg');
exit

3
.gitignore vendored
View File

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

View File

@@ -1,23 +0,0 @@
0 verbose cli /usr/bin/node /usr/bin/npm
1 info using npm@10.8.2
2 info using node@v18.20.8
3 silly config load:file:/usr/lib/node_modules/npm/npmrc
4 silly config load:file:/var/www/.npmrc
5 silly config load:file:/usr/etc/npmrc
6 verbose title npm run build
7 verbose argv "run" "build"
8 verbose logfile logs-max:10 dir:/var/www/.npm/_logs/2025-08-15T03_44_29_538Z-
9 verbose logfile /var/www/.npm/_logs/2025-08-15T03_44_29_538Z-debug-0.log
10 silly logfile done cleaning log files
11 http fetch GET 200 https://registry.npmjs.org/npm 197ms
12 verbose cwd /var/www
13 verbose os Linux 6.6.87.2-microsoft-standard-WSL2
14 verbose node v18.20.8
15 verbose npm v10.8.2
16 notice
16 notice New major version of npm available! 10.8.2 -> 11.5.2
16 notice Changelog: https://github.com/npm/cli/releases/tag/v11.5.2
16 notice To update run: npm install -g npm@11.5.2
16 notice { force: true, [Symbol(proc-log.meta)]: true }
17 verbose exit 0
18 info ok

View File

@@ -1,17 +0,0 @@
0 verbose cli /usr/bin/node /usr/bin/npm
1 info using npm@10.8.2
2 info using node@v18.20.8
3 silly config load:file:/usr/lib/node_modules/npm/npmrc
4 silly config load:file:/var/www/.npmrc
5 silly config load:file:/usr/etc/npmrc
6 verbose title npm run build
7 verbose argv "run" "build"
8 verbose logfile logs-max:10 dir:/var/www/.npm/_logs/2025-08-19T05_29_08_290Z-
9 verbose logfile /var/www/.npm/_logs/2025-08-19T05_29_08_290Z-debug-0.log
10 silly logfile done cleaning log files
11 verbose cwd /var/www
12 verbose os Linux 6.6.87.2-microsoft-standard-WSL2
13 verbose node v18.20.8
14 verbose npm v10.8.2
15 verbose exit 0
16 info ok

View File

@@ -1,17 +0,0 @@
0 verbose cli /usr/bin/node /usr/bin/npm
1 info using npm@10.8.2
2 info using node@v18.20.8
3 silly config load:file:/usr/lib/node_modules/npm/npmrc
4 silly config load:file:/var/www/.npmrc
5 silly config load:file:/usr/etc/npmrc
6 verbose title npm run build
7 verbose argv "run" "build"
8 verbose logfile logs-max:10 dir:/var/www/.npm/_logs/2025-08-19T10_49_06_806Z-
9 verbose logfile /var/www/.npm/_logs/2025-08-19T10_49_06_806Z-debug-0.log
10 silly logfile done cleaning log files
11 verbose cwd /var/www
12 verbose os Linux 6.6.87.2-microsoft-standard-WSL2
13 verbose node v18.20.8
14 verbose npm v10.8.2
15 verbose exit 0
16 info ok

View File

@@ -1,17 +0,0 @@
0 verbose cli /usr/bin/node /usr/bin/npm
1 info using npm@10.8.2
2 info using node@v18.20.8
3 silly config load:file:/usr/lib/node_modules/npm/npmrc
4 silly config load:file:/var/www/.npmrc
5 silly config load:file:/usr/etc/npmrc
6 verbose title npm run build
7 verbose argv "run" "build"
8 verbose logfile logs-max:10 dir:/var/www/.npm/_logs/2025-08-19T16_34_13_318Z-
9 verbose logfile /var/www/.npm/_logs/2025-08-19T16_34_13_318Z-debug-0.log
10 silly logfile done cleaning log files
11 verbose cwd /var/www
12 verbose os Linux 6.6.87.2-microsoft-standard-WSL2
13 verbose node v18.20.8
14 verbose npm v10.8.2
15 verbose exit 0
16 info ok

View File

@@ -1,17 +0,0 @@
0 verbose cli /usr/bin/node /usr/bin/npm
1 info using npm@10.8.2
2 info using node@v18.20.8
3 silly config load:file:/usr/lib/node_modules/npm/npmrc
4 silly config load:file:/var/www/.npmrc
5 silly config load:file:/usr/etc/npmrc
6 verbose title npm run build
7 verbose argv "run" "build"
8 verbose logfile logs-max:10 dir:/var/www/.npm/_logs/2025-08-19T16_41_22_490Z-
9 verbose logfile /var/www/.npm/_logs/2025-08-19T16_41_22_490Z-debug-0.log
10 silly logfile done cleaning log files
11 verbose cwd /var/www
12 verbose os Linux 6.6.87.2-microsoft-standard-WSL2
13 verbose node v18.20.8
14 verbose npm v10.8.2
15 verbose exit 0
16 info ok

View File

@@ -18,6 +18,9 @@ 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
@@ -64,6 +67,9 @@ RUN apt-get update && apt-get install -y \
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

View File

@@ -1,33 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Services\ServiceGoogleSheet;
use Illuminate\Console\Command;
class ScrapingLeaderData extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:scraping-leader-data';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Scraping leader data from google spreadsheet and save to database';
/**
* Execute the console command.
*/
public function handle()
{
$service_google_sheet = app(ServiceGoogleSheet::class);
$service_google_sheet->sync_leader_data();
$this->info('Leader data synced successfully');
}
}

View File

@@ -19,7 +19,7 @@ class SyncPbgTaskPayments extends Command
*
* @var string
*/
protected $description = 'Sync PBG task payments from Google Sheets REALISASI PAD';
protected $description = 'Sync PBG task payments from Google Sheets Sheet Data';
/**
* Execute the console command.
@@ -42,36 +42,13 @@ class SyncPbgTaskPayments extends Command
$this->newLine();
$this->table(
['Metric', 'Count'],
['Metric', 'Value'],
[
['Total rows processed', $result['total_rows']],
['Successful syncs', $result['successful_syncs']],
['Failed syncs', $result['failed_syncs']],
['Tasks not found', $result['not_found_tasks']],
['Inserted rows', $result['inserted'] ?? 0],
['Success', ($result['success'] ?? false) ? 'Yes' : 'No'],
]
);
// Show success rate
$success_rate = $result['total_rows'] > 0
? round(($result['successful_syncs'] / $result['total_rows']) * 100, 2)
: 0;
$this->info("📈 Success rate: {$success_rate}%");
// Show errors if any
if (!empty($result['errors'])) {
$this->newLine();
$this->warn('⚠️ Errors encountered:');
foreach (array_slice($result['errors'], 0, 5) as $error) {
$this->line(" Row {$error['row']} ({$error['registration_number']}): {$error['error']}");
}
if (count($result['errors']) > 5) {
$remaining = count($result['errors']) - 5;
$this->line(" ... and {$remaining} more errors (check logs for details)");
}
}
$this->newLine();
$this->info('📝 Check Laravel logs for detailed information.');

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

@@ -25,15 +25,16 @@ class BigDataResumeController extends Controller
{
try{
$filterDate = $request->get("filterByDate");
$type = $request->get("type");
$type = trim($request->get("type"));
if (!$filterDate || $filterDate === "latest") {
$big_data_resume = BigdataResume::latest()->first();
$big_data_resume = BigdataResume::where('resume_type', $type)->latest()->first();
if (!$big_data_resume) {
return $this->response_empty_resume();
}
} else {
$big_data_resume = BigdataResume::whereDate('created_at', $filterDate)
->where('resume_type', $type)
->orderBy('id', 'desc')
->first();
@@ -75,19 +76,19 @@ class BigDataResumeController extends Controller
$total_potensi_percentage = $big_data_resume->potention_sum > 0 && $target_pad > 0
? round(($big_data_resume->potention_sum / $target_pad) * 100, 2) : 0;
// percentage verified document (verified_sum / potention_sum) - by value/amount
$verified_percentage = $big_data_resume->potention_sum > 0 && $big_data_resume->verified_sum >= 0
? round(($big_data_resume->verified_sum / $big_data_resume->potention_sum) * 100, 2) : 0;
// // percentage verified document (verified_sum / potention_sum) - by value/amount
// $verified_percentage = $big_data_resume->potention_sum > 0 && $big_data_resume->verified_sum >= 0
// ? round(($big_data_resume->verified_sum / $big_data_resume->potention_sum) * 100, 2) : 0;
// percentage non-verified document (non_verified_sum / potention_sum) - by value/amount
$non_verified_percentage = $big_data_resume->potention_sum > 0 && $big_data_resume->non_verified_sum >= 0
? round(($big_data_resume->non_verified_sum / $big_data_resume->potention_sum) * 100, 2) : 0;
// // percentage non-verified document (non_verified_sum / potention_sum) - by value/amount
// $non_verified_percentage = $big_data_resume->potention_sum > 0 && $big_data_resume->non_verified_sum >= 0
// ? round(($big_data_resume->non_verified_sum / $big_data_resume->potention_sum) * 100, 2) : 0;
// Alternative: percentage by count (if needed)
// $verified_count_percentage = $big_data_resume->potention_count > 0
// ? round(($big_data_resume->verified_count / $big_data_resume->potention_count) * 100, 2) : 0;
// $non_verified_count_percentage = $big_data_resume->potention_count > 0
// ? round(($big_data_resume->non_verified_count / $big_data_resume->potention_count) * 100, 2) : 0;
$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
@@ -145,12 +146,12 @@ class BigDataResumeController extends Controller
'verified_document' => [
'sum' => (float) $big_data_resume->verified_sum,
'count' => $big_data_resume->verified_count,
'percentage' => $verified_percentage
'percentage' => $verified_count_percentage
],
'non_verified_document' => [
'sum' => (float) $big_data_resume->non_verified_sum,
'count' => $big_data_resume->non_verified_count,
'percentage' => $non_verified_percentage
'percentage' => $non_verified_count_percentage
],
'business_document' => [
'sum' => (float) $big_data_resume->business_sum,
@@ -485,14 +486,14 @@ class BigDataResumeController extends Controller
{
try {
// Get sum and count from PbgTaskPayment model
$totalSum = PbgTaskPayment::whereYear('payment_date', date('Y'))->sum('pad_amount') ?? 0;
$totalCount = PbgTaskPayment::whereYear('payment_date', date('Y'))->count() ?? 0;
$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();
Log::info("Real-time PBG Task Payments Data", [
'total_records' => $totalCount,
'total_sum' => $totalSum,
'source' => 'pbg_task_payments table'
]);
$totalSum = $stats->total_sum ?? 0;
$totalCount = $stats->total_count ?? 0;
return [
'sum' => (float) $totalSum,

View File

@@ -22,11 +22,11 @@ class LackOfPotentialController extends Controller
$total_tata_ruang_usaha = SpatialPlanning::where('building_function','like', '%usaha%')->count();
$total_tata_ruang_non_usaha = SpatialPlanning::where('building_function','not like', '%usaha%')->count();
$data_report_tourism = TourismBasedKBLI::all();
$data_pajak_reklame = Tax::where('tax_code','Reklame')->count();
$data_pajak_restoran = Tax::where('tax_code','Restoran')->count();
$data_pajak_hiburan = Tax::where('tax_code','Hiburan')->count();
$data_pajak_hotel = Tax::where('tax_code','Hotel')->count();
$data_pajak_parkir = Tax::where('tax_code','Parkir')->count();
$data_pajak_reklame = Tax::where('tax_code','Reklame')->distinct('business_name')->count();
$data_pajak_restoran = Tax::where('tax_code','Restoran')->distinct('business_name')->count();
$data_pajak_hiburan = Tax::where('tax_code','Hiburan')->distinct('business_name')->count();
$data_pajak_hotel = Tax::where('tax_code','Hotel')->distinct('business_name')->count();
$data_pajak_parkir = Tax::where('tax_code','Parkir')->distinct('business_name')->count();
return response()->json([
'total_reklame' => $total_reklame,

View File

@@ -12,7 +12,6 @@ use App\Models\DataSetting;
use App\Models\ImportDatasource;
use App\Models\PbgTask;
use App\Models\PbgTaskGoogleSheet;
use App\Services\GoogleSheetService;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@@ -20,10 +19,6 @@ use Illuminate\Validation\Rules\Enum;
class PbgTaskController extends Controller
{
protected $googleSheetService;
public function __construct(GoogleSheetService $googleSheetService){
$this->googleSheetService = $googleSheetService;
}
public function index(Request $request)
{
info($request);

View File

@@ -56,7 +56,9 @@ class RequestAssignmentController extends Controller
'attachments' => function ($q) {
$q->whereIn('pbg_type', ['berita_acara', 'bukti_bayar']);
},
'pbg_task_retributions'
'pbg_task_retributions',
'pbg_task_detail',
'pbg_status'
])->orderBy('id', 'desc');
// Log final query count for debugging
@@ -126,10 +128,10 @@ class RequestAssignmentController extends Controller
->whereIn("status", PbgTaskStatus::getNonVerified())
// Additional condition: unit IS NULL OR unit <= 1
->where(function ($q3) {
$q3->whereDoesntHave('pbg_task_details', function ($q4) {
$q3->whereDoesntHave('pbg_task_detail', function ($q4) {
$q4->where('unit', '>', 1);
})
->orWhereDoesntHave('pbg_task_details');
->orWhereDoesntHave('pbg_task_detail');
});
});
break;
@@ -152,7 +154,7 @@ class RequestAssignmentController extends Controller
})
->orWhereNull('function_type');
})
->whereHas('pbg_task_details', function ($q4) {
->whereHas('pbg_task_detail', function ($q4) {
$q4->where('unit', '>', 1);
});
});
@@ -204,10 +206,10 @@ class RequestAssignmentController extends Controller
->whereIn("status", PbgTaskStatus::getNonVerified())
// Additional condition: unit IS NULL OR unit <= 1
->where(function ($q3) {
$q3->whereDoesntHave('pbg_task_details', function ($q4) {
$q3->whereDoesntHave('pbg_task_detail', function ($q4) {
$q4->where('unit', '>', 1);
})
->orWhereDoesntHave('pbg_task_details');
->orWhereDoesntHave('pbg_task_detail');
});
})
->whereExists(function ($query) {
@@ -232,10 +234,10 @@ class RequestAssignmentController extends Controller
->whereIn("status", PbgTaskStatus::getNonVerified())
// Additional condition: unit IS NULL OR unit <= 1
->where(function ($q3) {
$q3->whereDoesntHave('pbg_task_details', function ($q4) {
$q3->whereDoesntHave('pbg_task_detail', function ($q4) {
$q4->where('unit', '>', 1);
})
->orWhereDoesntHave('pbg_task_details');
->orWhereDoesntHave('pbg_task_detail');
});
})
->whereExists(function ($query) {
@@ -265,7 +267,7 @@ class RequestAssignmentController extends Controller
})
->orWhereNull('function_type');
})
->whereHas('pbg_task_details', function ($q4) {
->whereHas('pbg_task_detail', function ($q4) {
$q4->where('unit', '>', 1);
});
});
@@ -299,7 +301,7 @@ class RequestAssignmentController extends Controller
})
->orWhereNull('function_type');
})
->whereHas('pbg_task_details', function ($q4) {
->whereHas('pbg_task_detail', function ($q4) {
$q4->where('unit', '>', 1);
});
});
@@ -333,7 +335,7 @@ class RequestAssignmentController extends Controller
})
->orWhereNull('function_type');
})
->whereHas('pbg_task_details', function ($q4) {
->whereHas('pbg_task_detail', function ($q4) {
$q4->where('unit', '>', 1);
});
});
@@ -528,7 +530,7 @@ class RequestAssignmentController extends Controller
})
->orWhereNull('function_type');
})
->whereHas('pbg_task_details', function ($q4) {
->whereHas('pbg_task_detail', function ($q4) {
$q4->where('unit', '>', 1);
});
});
@@ -552,10 +554,10 @@ class RequestAssignmentController extends Controller
->whereIn("status", PbgTaskStatus::getNonVerified())
// Additional condition: unit IS NULL OR unit <= 1
->where(function ($q3) {
$q3->whereDoesntHave('pbg_task_details', function ($q4) {
$q3->whereDoesntHave('pbg_task_detail', function ($q4) {
$q4->where('unit', '>', 1);
})
->orWhereDoesntHave('pbg_task_details');
->orWhereDoesntHave('pbg_task_detail');
});
})
->where('is_valid', true)

View File

@@ -54,7 +54,10 @@ class AuthenticatedSessionController extends Controller
session(['login_timestamp' => now()->timestamp]);
session(['user_id' => $user->id]);
return redirect()->intended(RouteServiceProvider::HOME);
// 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);
}
/**

View File

@@ -9,6 +9,7 @@ use App\Models\PbgTask;
use App\Models\TaskAssignment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
class QuickSearchController extends Controller
{
@@ -16,6 +17,10 @@ class QuickSearchController extends Controller
return view("quick-search.index");
}
public function public_search(){
return view("public-search.index");
}
public function search_result(Request $request){
$keyword = $request->get("keyword");
@@ -25,9 +30,14 @@ class QuickSearchController extends Controller
public function quick_search_datatable(Request $request)
{
try {
$query = PbgTask::leftJoin('pbg_task_details', 'pbg_task.uuid', '=', 'pbg_task_details.pbg_task_uid')
->select('pbg_task.*')
->orderBy('pbg_task.id', 'desc');
// Gunakan subquery untuk performa yang lebih baik dan menghindari duplikasi
$query = PbgTask::select([
'pbg_task.*',
DB::raw('(SELECT name_building FROM pbg_task_details WHERE pbg_task_details.pbg_task_uid = pbg_task.uuid LIMIT 1) as name_building'),
DB::raw('(SELECT nilai_retribusi_bangunan FROM pbg_task_retributions WHERE pbg_task_retributions.pbg_task_uid = pbg_task.uuid LIMIT 1) as nilai_retribusi_bangunan'),
DB::raw('(SELECT note FROM pbg_statuses WHERE pbg_statuses.pbg_task_uuid = pbg_task.uuid LIMIT 1) as note')
])
->orderBy('pbg_task.id', 'desc');
if ($request->filled('search')) {
$search = trim($request->get('search'));
@@ -36,7 +46,12 @@ class QuickSearchController extends Controller
->orWhere('pbg_task.name', 'LIKE', "%$search%")
->orWhere('pbg_task.owner_name', 'LIKE', "%$search%")
->orWhere('pbg_task.address', 'LIKE', "%$search%")
->orWhere('pbg_task_details.name_building', '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%");
});
});
}
@@ -50,13 +65,85 @@ class QuickSearchController extends Controller
}
}
public function public_search_datatable(Request $request)
{
try {
// Hanya proses jika ada keyword search
if (!$request->filled('search') || trim($request->get('search')) === '') {
return response()->json([
'data' => [],
'total' => 0,
'current_page' => 1,
'last_page' => 1,
'per_page' => 15,
'from' => null,
'to' => null
]);
}
$search = trim($request->get('search'));
// Validasi minimal 3 karakter
if (strlen($search) < 3) {
return response()->json([
'data' => [],
'total' => 0,
'current_page' => 1,
'last_page' => 1,
'per_page' => 15,
'from' => null,
'to' => null,
'message' => 'Minimal 3 karakter untuk pencarian'
]);
}
// Gunakan subquery untuk performa yang lebih baik dan menghindari duplikasi
$query = PbgTask::select([
'pbg_task.*',
DB::raw('(SELECT name_building FROM pbg_task_details WHERE pbg_task_details.pbg_task_uid = pbg_task.uuid LIMIT 1) as name_building'),
DB::raw('(SELECT nilai_retribusi_bangunan FROM pbg_task_retributions WHERE pbg_task_retributions.pbg_task_uid = pbg_task.uuid LIMIT 1) as nilai_retribusi_bangunan'),
DB::raw('(SELECT note FROM pbg_statuses WHERE pbg_statuses.pbg_task_uuid = pbg_task.uuid LIMIT 1) as note')
])
->where(function ($q) use ($search) {
$q->where('pbg_task.registration_number', 'LIKE', "%$search%")
->orWhere('pbg_task.name', 'LIKE', "%$search%")
->orWhere('pbg_task.owner_name', 'LIKE', "%$search%")
->orWhere('pbg_task.address', 'LIKE', "%$search%")
->orWhereExists(function ($subQuery) use ($search) {
$subQuery->select(DB::raw(1))
->from('pbg_task_details')
->whereColumn('pbg_task_details.pbg_task_uid', 'pbg_task.uuid')
->where('pbg_task_details.name_building', 'LIKE', "%$search%");
});
})
->orderBy('pbg_task.id', 'desc');
$result = $query->paginate();
// Tambahkan message jika tidak ada hasil
if ($result->total() === 0) {
$result = $result->toArray();
$result['message'] = 'Tidak ada data yang ditemukan';
}
return response()->json($result);
} catch (\Throwable $e) {
Log::error("Error fetching datatable data: " . $e->getMessage());
return response()->json([
'message' => 'Terjadi kesalahan saat mengambil data.',
'error' => $e->getMessage(),
], 500);
}
}
public function show($id)
{
try {
$data = PbgTask::with([
'pbg_task_retributions',
'pbg_task_index_integrations',
'pbg_task_retributions.pbg_task_prasarana'
'pbg_task_retributions.pbg_task_prasarana',
'pbg_status'
])->findOrFail($id);
$statusOptions = PbgTaskStatus::getStatuses();

View File

@@ -32,7 +32,7 @@ class PbgTaskController extends Controller
'destroyer' => $destroyer,
'filter' => $filter,
'filterOptions' => PbgTaskFilterData::getAllOptions(),
]);
]);
}
/**
@@ -61,6 +61,7 @@ class PbgTaskController extends Controller
'pbg_task_index_integrations',
'pbg_task_retributions.pbg_task_prasarana',
'pbg_task_detail',
'pbg_status',
'dataLists' => function($query) {
$query->orderBy('data_type')->orderBy('name');
}
@@ -69,20 +70,6 @@ class PbgTaskController extends Controller
// Group data lists by data_type for easier display
$dataListsByType = $data->dataLists->groupBy('data_type');
// Debug: Log the data types found for this task
\Log::info('PBG Task Data Lists', [
'task_uuid' => $data->uuid,
'total_data_lists' => $data->dataLists->count(),
'data_types_found' => $dataListsByType->keys()->toArray(),
'data_types_with_names' => $dataListsByType->map(function($items, $type) {
return [
'type' => $type,
'name' => $items->first()->data_type_name ?? "Type {$type}",
'count' => $items->count()
];
})->values()->toArray()
]);
$statusOptions = PbgTaskStatus::getStatuses();
$applicationTypes = PbgTaskApplicationTypes::labels();

View File

@@ -3,17 +3,12 @@
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use App\Services\ServiceSIMBG;
use Illuminate\Http\Request;
use Exception;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class SyncronizeController extends Controller
{
protected $service_simbg;
public function __construct(ServiceSIMBG $service_simbg){
$this->service_simbg = $service_simbg;
}
public function index(Request $request){
$menuId = $request->query('menu_id');
$user = Auth::user();
@@ -37,36 +32,4 @@ class SyncronizeController extends Controller
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

@@ -22,6 +22,11 @@ class MenuResource extends JsonResource
'url' => $this->url,
'sort_order' => $this->sort_order,
'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,
'updated_at' => $this->updated_at
];

View File

@@ -43,6 +43,8 @@ class RequestAssignmentResouce extends JsonResource
->sortByDesc('created_at')
->first(),
'pbg_task_retributions' => $this->pbg_task_retributions,
'pbg_task_detail' => $this->pbg_task_detail,
'pbg_status' => $this->pbg_status,
];
}
}

View File

@@ -59,28 +59,12 @@ class ScrapingDataJob implements ShouldQueue
'start_time' => now(),
'failed_uuid' => null
]);
Log::info("ImportDatasource created", ['id' => $import_datasource->id]);
// STEP 1: Scrape Google Sheet data first
Log::info("=== STEP 1: SCRAPING GOOGLE SHEET ===");
$import_datasource->update(['message' => 'Scraping Google Sheet data...']);
$service_google_sheet->run_service();
Log::info("Google Sheet scraping completed successfully");
// STEP 2: Scrape PBG Task to get parent data
Log::info("=== STEP 2: SCRAPING PBG TASK PARENT DATA ===");
$import_datasource->update(['message' => 'Scraping PBG Task parent data...']);
$service_pbg_task->run_service();
Log::info("PBG Task parent data scraping completed");
// STEP 3: Get all PBG tasks for detail scraping
$totalTasks = PbgTask::count();
Log::info("=== STEP 3: SCRAPING PBG TASK DETAILS ===", [
'total_tasks' => $totalTasks
]);
$import_datasource->update([
'message' => "Scraping details for {$totalTasks} PBG tasks..."
@@ -135,13 +119,10 @@ class ScrapingDataJob implements ShouldQueue
}
});
Log::info("Task details scraping completed", [
'processed_tasks' => $processedTasks,
'total_tasks' => $totalTasks
]);
$import_datasource->update(['message' => 'Scraping Google Sheet data...']);
$service_google_sheet->run_service();
// STEP 4: Generate BigData Resume
Log::info("=== STEP 4: GENERATING BIGDATA RESUME ===");
$import_datasource->update(['message' => 'Generating BigData resume...']);
BigdataResume::generateResumeData($import_datasource->id, date('Y'), "simbg");
@@ -200,6 +181,7 @@ class ScrapingDataJob implements ShouldQueue
$service->scraping_pbg_data_list($uuid);
$service->scraping_task_retributions($uuid);
$service->scraping_task_integrations($uuid);
$service->scraping_task_detail_status($uuid);
}
/**

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

@@ -65,6 +65,11 @@ class PbgTask extends Model
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
*/

View File

@@ -11,15 +11,79 @@ class PbgTaskPayment extends Model
protected $fillable = [
'pbg_task_id',
'pbg_task_uid',
'registration_number',
'sts_form_number',
'payment_date',
'pad_amount'
// 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 = [
'payment_date' => 'date',
'pad_amount' => 'decimal:2'
'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'
];
/**

View File

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

View File

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

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

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

@@ -13,6 +13,9 @@ use Exception;
use Google\Client as Google_Client;
use Google\Service\Sheets as Google_Service_Sheets;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use App\Models\PbgTask;
class ServiceGoogleSheet
{
protected $client;
@@ -36,8 +39,8 @@ class ServiceGoogleSheet
public function run_service(){
try{
// $this->sync_big_data();
$this->sync_google_sheet_data();
$this->sync_pbg_task_payments();
}catch(Exception $e){
throw $e;
}
@@ -248,7 +251,6 @@ class ServiceGoogleSheet
]);
try {
$sections = [
'TARGET_PAD' => "TARGET PAD 2024",
'KEKURANGAN_POTENSI' => "DEVIASI TARGET DENGAN POTENSI TOTAL BERKAS",
'TOTAL_POTENSI_BERKAS' => "•TOTAL BERKAS 2025",
'BELUM_TERVERIFIKASI' => "•BERKAS AKTUAL BELUM TERVERIFIKASI (POTENSI):",
@@ -314,7 +316,6 @@ class ServiceGoogleSheet
// Save data settings
$dataSettings = [
'TARGET_PAD' => $result['TARGET_PAD']['nominal'] ?? null,
'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,
@@ -363,7 +364,6 @@ class ServiceGoogleSheet
public function get_big_resume_data(){
try {
$sections = [
'TARGET_PAD' => "TARGET PAD 2024",
'KEKURANGAN_POTENSI' => "DEVIASI TARGET DENGAN POTENSI TOTAL BERKAS",
'TOTAL_POTENSI_BERKAS' => "•TOTAL BERKAS 2025",
'BELUM_TERVERIFIKASI' => "•BERKAS AKTUAL BELUM TERVERIFIKASI (POTENSI):",
@@ -391,7 +391,6 @@ class ServiceGoogleSheet
// Save data settings
$dataSettings = [
'TARGET_PAD' => $this->convertToDecimal($result['TARGET_PAD']['nominal']) ?? 0,
'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,
@@ -427,196 +426,427 @@ class ServiceGoogleSheet
}
}
public function get_realisasi_pad_data(){
/**
* 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 {
// Get data from "REALISASI PAD" sheet
$sheet_data = $this->get_data_by_sheet_name("REALISASI PAD");
$sheet_data = $this->get_data_by_sheet_name($sheet_name);
if (empty($sheet_data)) {
Log::warning("No data found in REALISASI PAD sheet");
return [];
Log::warning("No data found in sheet", ['sheet_name' => $sheet_name]);
return [
'headers' => [],
'data' => [],
'selected_columns' => []
];
}
// Column indices: C=2, AK=36, AL=37, AW=48 (0-based)
$columns = [
'C' => 2,
'AK' => 36,
'AL' => 37,
'AW' => 48
];
// 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' => []
'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
foreach ($columns as $column_name => $column_index) {
$result['headers'][$column_name] = isset($row[$column_index]) ? trim($row[$column_index]) : '';
// 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 {
// Data rows
$row_data = [
'row' => $row_index + 1 // 1-based row number
];
$row_assoc = [];
$has_data = false;
foreach ($columns as $column_name => $column_index) {
$value = isset($row[$column_index]) ? trim($row[$column_index]) : '';
$row_data[$column_name] = $value;
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;
}
}
// Only add row if it has at least one non-empty value
if ($has_data) {
$result['data'][] = $row_data;
$result['data'][] = $row_assoc;
}
}
}
Log::info("REALISASI PAD Multiple Columns Data", [
'sheet_name' => 'REALISASI PAD',
'columns' => array_keys($columns),
'headers' => $result['headers'],
'total_data_rows' => count($result['data']),
'sample_data' => array_slice($result['data'], 0, 5), // Show first 5 rows as sample
'column_indices' => $columns
]);
return $result;
} catch (\Exception $e) {
Log::error("Error getting REALISASI PAD data", ['error' => $e->getMessage()]);
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 {
// Get payment data from REALISASI PAD sheet
$payment_data = $this->get_realisasi_pad_data();
if (empty($payment_data['data'])) {
Log::warning("No payment data found to sync");
return ['success' => false, 'message' => 'No payment data found'];
$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'];
}
$successful_syncs = 0;
$failed_syncs = 0;
$not_found_tasks = 0;
$not_found_tasks_registration_number = [];
$failed_sync_registration_numbers = [];
$errors = [];
// 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);
foreach ($payment_data['data'] as $row) {
try {
// Clean registration number from column C
$registration_number = \App\Models\PbgTaskPayment::cleanRegistrationNumber($row['C']);
if (empty($registration_number)) {
$failed_syncs++;
$failed_sync_registration_numbers[] = [
'row' => $row['row'],
'registration_number' => $row['C'] ?? 'EMPTY',
'reason' => 'Empty registration number'
];
continue;
}
// Find PBG task by registration number
$pbg_task = \App\Models\PbgTask::where('registration_number', $registration_number)->first();
if (!$pbg_task) {
$not_found_tasks_registration_number[] = [
'row' => $row['row'],
'registration_number' => $registration_number
];
$not_found_tasks++;
Log::warning("PBG Task not found for registration number", [
'registration_number' => $registration_number,
'row' => $row['row']
]);
continue;
}
// Convert data types
$payment_date = \App\Models\PbgTaskPayment::convertPaymentDate($row['AK']);
$pad_amount = \App\Models\PbgTaskPayment::convertPadAmount($row['AW']);
$sts_form_number = !empty($row['AL']) ? trim($row['AL']) : null;
// Create or update payment record
\App\Models\PbgTaskPayment::updateOrCreate(
[
'pbg_task_id' => $pbg_task->id,
'registration_number' => $registration_number
],
[
'pbg_task_uid' => $pbg_task->uuid,
'sts_form_number' => $sts_form_number,
'payment_date' => $payment_date,
'pad_amount' => $pad_amount
]
);
$successful_syncs++;
} catch (\Exception $e) {
$failed_syncs++;
$registration_number = $row['C'] ?? 'N/A';
$failed_sync_registration_numbers[] = [
'row' => $row['row'],
'registration_number' => $registration_number,
'reason' => $e->getMessage()
];
$errors[] = [
'row' => $row['row'],
'registration_number' => $registration_number,
'error' => $e->getMessage()
];
Log::error("Error syncing payment data for row", [
'row' => $row['row'],
'registration_number' => $registration_number,
'error' => $e->getMessage()
]);
}
// 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);
}
$result = [
'success' => true,
'total_rows' => count($payment_data['data']),
'successful_syncs' => $successful_syncs,
'failed_syncs' => $failed_syncs,
'not_found_tasks' => $not_found_tasks,
'not_found_tasks_registration_number' => $not_found_tasks_registration_number,
'failed_sync_registration_numbers' => $failed_sync_registration_numbers,
'errors' => $errors
// 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'
];
Log::info("PBG Task Payments sync completed", $result);
$normalizedHeaderValues = array_values($headers);
$overlap = array_intersect($expectedHeaders, $normalizedHeaderValues);
// Log detailed arrays for failed syncs and not found registration numbers
if (!empty($failed_sync_registration_numbers)) {
Log::warning("Failed Sync Registration Numbers", [
'count' => count($failed_sync_registration_numbers),
'details' => $failed_sync_registration_numbers
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'];
}
if (!empty($not_found_tasks_registration_number)) {
Log::warning("Not Found Registration Numbers", [
'count' => count($not_found_tasks_registration_number),
'details' => $not_found_tasks_registration_number
// 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(); }
}
return $result;
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()]);

View File

@@ -1,662 +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, date('Y'), "simbg");
// 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,6 +3,7 @@
namespace App\Services;
use App\Models\GlobalSetting;
use App\Models\PbgStatus;
use App\Models\PbgTask;
use App\Models\PbgTaskDetail;
use App\Models\PbgTaskDetailDataList;
@@ -220,6 +221,75 @@ class ServiceTabPbgTask
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";

View File

@@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('pbg_statuses', function (Blueprint $table) {
$table->id();
$table->string('pbg_task_uuid');
$table->integer('status');
$table->string('status_name');
$table->integer('slf_status')->nullable();
$table->string('slf_status_name')->nullable();
$table->date('due_date')->nullable();
// nested "data"
$table->string('uid')->nullable();
$table->text('note')->nullable();
$table->string('file')->nullable();
$table->date('data_due_date')->nullable();
$table->timestamp('data_created_at')->nullable();
$table->json('slf_data')->nullable(); // kalau nanti slf_data ada struktur JSON
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('pbg_statuses');
}
};

View File

@@ -0,0 +1,149 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('pbg_task_payments', function (Blueprint $table) {
// Drop existing foreign key if present
if (Schema::hasColumn('pbg_task_payments', 'pbg_task_id')) {
$table->dropForeign(['pbg_task_id']);
// Make column nullable
$table->unsignedBigInteger('pbg_task_id')->nullable()->change();
// Recreate foreign key
$table->foreign('pbg_task_id')->references('id')->on('pbg_task')->cascadeOnDelete();
}
// Drop legacy columns if no longer needed
if (Schema::hasColumn('pbg_task_payments', 'registration_number')) {
$table->dropIndex(['registration_number']);
$table->dropColumn('registration_number');
}
if (Schema::hasColumn('pbg_task_payments', 'sts_form_number')) {
$table->dropColumn('sts_form_number');
}
if (Schema::hasColumn('pbg_task_payments', 'payment_date')) {
$table->dropColumn('payment_date');
}
if (Schema::hasColumn('pbg_task_payments', 'pad_amount')) {
$table->dropColumn('pad_amount');
}
// Make pbg_task_uid nullable
if (Schema::hasColumn('pbg_task_payments', 'pbg_task_uid')) {
$table->string('pbg_task_uid')->nullable()->change();
}
// Add new columns (renamed for table conventions)
$table->integer('row_no')->nullable();
$table->string('consultation_type')->nullable();
$table->string('source_registration_number')->nullable();
$table->string('owner_name')->nullable();
$table->text('building_location')->nullable();
$table->string('building_function')->nullable();
$table->string('building_name')->nullable();
$table->date('application_date_raw')->nullable();
$table->string('verification_status')->nullable();
$table->string('application_status')->nullable();
$table->text('owner_address')->nullable();
$table->string('owner_phone')->nullable();
$table->string('owner_email')->nullable();
$table->date('note_date_raw')->nullable();
$table->text('document_shortage_note')->nullable();
$table->string('image_url')->nullable();
$table->string('krk_kkpr')->nullable();
$table->string('krk_number')->nullable();
$table->string('lh')->nullable();
$table->string('ska')->nullable();
$table->string('remarks')->nullable();
$table->string('helpdesk')->nullable();
$table->string('person_in_charge')->nullable();
$table->string('pbg_operator')->nullable();
$table->string('ownership')->nullable();
$table->string('taru_potential')->nullable();
$table->string('agency_validation')->nullable();
$table->string('retribution_category')->nullable();
$table->string('ba_tpt_number')->nullable();
$table->date('ba_tpt_date_raw')->nullable();
$table->string('ba_tpa_number')->nullable();
$table->date('ba_tpa_date_raw')->nullable();
$table->string('skrd_number')->nullable();
$table->date('skrd_date_raw')->nullable();
$table->string('ptsp_status')->nullable();
$table->string('issued_status')->nullable();
$table->date('payment_date_raw')->nullable();
$table->string('sts_format')->nullable();
$table->integer('issuance_year')->nullable();
$table->integer('current_year')->nullable();
$table->string('village')->nullable();
$table->string('district')->nullable();
$table->decimal('building_area', 18, 2)->nullable()->default(0);
$table->decimal('building_height', 18, 2)->nullable()->default(0);
$table->integer('floor_count')->nullable();
$table->integer('unit_count')->nullable();
$table->decimal('proposed_retribution', 18, 2)->nullable()->default(0);
$table->decimal('retribution_total_simbg', 18, 2)->nullable()->default(0);
$table->decimal('retribution_total_pad', 18, 2)->nullable()->default(0);
$table->decimal('penalty_amount', 18, 2)->nullable()->default(0);
$table->string('business_category')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('pbg_task_payments', function (Blueprint $table) {
if (Schema::hasColumn('pbg_task_payments', 'pbg_task_id')) {
// Drop FK, revert to not nullable, recreate FK
$table->dropForeign(['pbg_task_id']);
$table->unsignedBigInteger('pbg_task_id')->nullable(false)->change();
$table->foreign('pbg_task_id')->references('id')->on('pbg_task')->cascadeOnDelete();
}
// Revert pbg_task_uid to not nullable
if (Schema::hasColumn('pbg_task_payments', 'pbg_task_uid')) {
$table->string('pbg_task_uid')->nullable(false)->change();
}
// Drop the added columns
$columns = [
'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'
];
foreach ($columns as $col) {
if (Schema::hasColumn('pbg_task_payments', $col)) {
$table->dropColumn($col);
}
}
// Restore legacy columns
if (!Schema::hasColumn('pbg_task_payments', 'registration_number')) {
$table->string('registration_number');
$table->index('registration_number');
}
if (!Schema::hasColumn('pbg_task_payments', 'sts_form_number')) {
$table->string('sts_form_number')->nullable();
}
if (!Schema::hasColumn('pbg_task_payments', 'payment_date')) {
$table->date('payment_date')->nullable();
}
if (!Schema::hasColumn('pbg_task_payments', 'pad_amount')) {
$table->decimal('pad_amount', 18, 2)->default(0);
}
});
}
};

View File

@@ -32,7 +32,7 @@ class MenuSeeder extends Seeder
"sort_order" => 2,
"children" => [
[
"name" => "Dashboard Pimpinan (PBG)",
"name" => "Dashboard Pimpinan (SIMBG)",
"url" => "dashboard.home",
"icon" => null,
"sort_order" => 1,
@@ -69,12 +69,6 @@ class MenuSeeder extends Seeder
"icon" => null,
"sort_order" => 4,
],
[
"name" => "Dashboard Pimpinan",
"url" => "dashboard.leader",
"icon" => null,
"sort_order" => 5,
],
],
],
[
@@ -288,7 +282,7 @@ class MenuSeeder extends Seeder
private function createOrUpdateMenu($menuData, $parentId = null){
$menuData['parent_id'] = $parentId;
$menu = Menu::updateOrCreate(['name' => $menuData['name']], Arr::except($menuData, ['children']));
$menu = Menu::updateOrCreate(['url' => $menuData['url']], Arr::except($menuData, ['children']));
if(!empty($menuData['children'])){
foreach($menuData['children'] as $child){

View File

@@ -21,7 +21,7 @@ class UsersRoleMenuSeeder extends Seeder
// Fetch all menus in a single query and index by name
$menus = Menu::whereIn('name', [
'Dashboard', 'Master', 'Settings', 'Data Settings', 'Data', 'Laporan', 'Neng Bedas',
'Approval', 'Tools', 'Dashboard Pimpinan', 'Dashboard PBG', 'Users', 'Syncronize',
'Approval', 'Tools', 'Users', 'Syncronize', 'Dashboard Pimpinan (SIMBG)',
'Menu', 'Role', 'Setting Dashboard', 'PBG', 'Reklame', 'Usaha atau Industri', 'Pariwisata',
'Lap Pariwisata', 'UMKM', 'Dashboard Potensi', 'Tata Ruang', 'PDAM', 'PETA',
'Lap Pimpinan', 'Dalam Sistem', 'Luar Sistem', 'Google Sheets', 'TPA TPT', 'Pajak',
@@ -32,14 +32,14 @@ class UsersRoleMenuSeeder extends Seeder
$permissions = [
'superadmin' => [
'Dashboard', 'Master', 'Settings', 'Data Settings', 'Data', 'Laporan', 'Neng Bedas',
'Approval', 'Tools', 'Dashboard Pimpinan', 'Dashboard PBG', 'Users', 'Syncronize',
'Approval', 'Tools', 'Users', 'Syncronize', 'Dashboard Pimpinan (SIMBG)',
'Menu', 'Role', 'Setting Dashboard', 'PBG', 'Reklame', 'Usaha atau Industri', 'Pariwisata',
'Lap Pariwisata', 'UMKM', 'Dashboard Potensi', 'Tata Ruang', 'PDAM', 'Dalam Sistem',
'Luar Sistem', 'Lap Pimpinan', 'Google Sheets', 'TPA TPT', 'Approval Pejabat',
'Undangan', 'Rekap Pembayaran', 'Lap Rekap Data Pembayaran', 'Lap PBG (PTSP)', 'Lap Pertumbuhan', 'Pajak'
],
'user' => ['Dashboard', 'Data', 'Laporan', 'Neng Bedas',
'Approval', 'Tools', 'Dashboard Pimpinan', 'Dashboard PBG', 'Users', 'Syncronize',
'Approval', 'Tools', 'Users', 'Syncronize', 'Dashboard Pimpinan (SIMBG)',
'Menu', 'Role', 'Setting Dashboard', 'PBG', 'Reklame', 'Usaha atau Industri', 'Pariwisata',
'Lap Pariwisata', 'UMKM', 'Dashboard Potensi', 'Tata Ruang', 'PDAM', 'Dalam Sistem',
'Luar Sistem', 'Lap Pimpinan', 'Google Sheets', 'TPA TPT', 'Approval Pejabat',

View File

@@ -0,0 +1 @@
memory_limit=512M

View File

@@ -565,12 +565,11 @@ class BigData {
document
.querySelectorAll(".document-total.chart-payment-pbg-task")
.forEach((element) => {
// const sum = this.safeGet(
// this.resumeBigData,
// "pbg_task_payments.sum",
// 0
// );
const sum = 9559353945;
const sum = this.safeGet(
this.resumeBigData,
"pbg_task_payments.sum",
0
);
element.innerText = `Rp.${addThousandSeparators(
sum.toString()
)}`;

View File

@@ -306,23 +306,23 @@ class DashboardPotentialInsideSystem {
safeSetText(
"restoran-count-amount",
addThousandSeparators(
(this.pajakRestoranCount * 2500000).toString()
(this.pajakRestoranCount * 6200000).toString()
)
);
safeSetText("hiburan-count", this.pajakHiburanCount);
safeSetText(
"hiburan-count-amount",
addThousandSeparators((this.pajakHiburanCount * 2500000).toString())
addThousandSeparators((this.pajakHiburanCount * 6200000).toString())
);
safeSetText("hotel-count", this.pajakHotelCount);
safeSetText(
"hotel-count-amount",
addThousandSeparators(this.pajakHotelCount * 2500000).toString()
addThousandSeparators(this.pajakHotelCount * 6200000).toString()
);
safeSetText("parkir-count", this.pajakParkirCount);
safeSetText(
"parkir-count-amount",
addThousandSeparators((this.pajakParkirCount * 2500000).toString())
addThousandSeparators((this.pajakParkirCount * 6200000).toString())
);
safeSetText("pdam-count", this.pdamCount);
safeSetText(

View File

@@ -113,11 +113,15 @@ class PbgTasks {
{ name: "Alamat" },
"Status",
"Jenis Fungsi",
{ name: "Nama Bangunan" },
"Jenis Konsultasi",
{ name: "Tanggal Jatuh Tempo" },
{
name: "Retribusi",
},
{
name: "Catatan Kekurangan Dokumen",
},
{
name: "Aksi",
formatter: (cell) => {
@@ -211,26 +215,32 @@ class PbgTasks {
"Content-Type": "application/json",
},
then: (data) =>
data.data.map((item) => [
item.id,
item.name,
item.owner_name,
item.condition,
item.registration_number,
item.document_number,
item.address,
item.status_name,
item.function_type,
item.consultation_type,
item.due_date,
item.pbg_task_retributions
? addThousandSeparators(
item.pbg_task_retributions
.nilai_retribusi_bangunan
)
: "-",
item,
]),
data.data.map((item) => {
return [
item.id,
item.name,
item.owner_name,
item.condition,
item.registration_number,
item.document_number || "-",
item.address,
item.status_name,
item.function_type,
item.pbg_task_detail
? item.pbg_task_detail.name_building
: "-",
item.consultation_type,
item.due_date,
item.pbg_task_retributions
? addThousandSeparators(
item.pbg_task_retributions
.nilai_retribusi_bangunan
)
: "-",
item.pbg_status ? item.pbg_status.note : "-",
item,
];
}),
total: (data) => data.meta.total,
},
};

View File

@@ -0,0 +1,312 @@
import { Grid, html } from "gridjs";
import { addThousandSeparators } from "../global-config";
class PublicSearch {
constructor() {
this.table = null;
const baseInput = document.getElementById("base_url_datatable");
this.baseUrl = baseInput ? baseInput.value.split("?")[0] : "";
this.keywordInput = document.getElementById("search_input");
this.searchButton = document.getElementById("search_button");
this.searchHeader = document.getElementById("search-header");
this.tableWrapper = document.getElementById("table-wrapper");
this.emptyState = document.getElementById("empty-state");
// Tidak inisialisasi datatable sampai ada pencarian
this.datatableUrl = null;
}
init() {
this.bindSearchButton();
// Check if there's a keyword in URL
const urlParams = new URLSearchParams(window.location.search);
const keyword = urlParams.get("keyword");
if (keyword && keyword.trim() !== "") {
this.keywordInput.value = keyword.trim();
this.handleSearchFromUrl(keyword.trim());
}
}
handleSearchFromUrl(keyword) {
// Validasi input kosong atau hanya spasi
if (!keyword || keyword.trim().length === 0) {
this.showEmptyState("Mulai Pencarian");
return;
}
// Validasi minimal 3 karakter
if (keyword.trim().length < 3) {
this.showEmptyState("Minimal 3 karakter untuk pencarian");
return;
}
this.datatableUrl = this.buildUrl(keyword.trim());
this.showSearchResults();
this.initDatatable();
}
bindSearchButton() {
const handleSearch = () => {
const newKeyword = this.keywordInput.value.trim();
// Validasi input kosong atau hanya spasi
if (!newKeyword || newKeyword.length === 0) {
this.showEmptyState("Mulai Pencarian");
return;
}
// Validasi minimal 3 karakter (setelah trim)
if (newKeyword.length < 3) {
this.showEmptyState("Minimal 3 karakter untuk pencarian");
return;
}
// 1. Update datatable URL and reload
this.datatableUrl = this.buildUrl(newKeyword);
this.showSearchResults();
this.initDatatable();
// 2. Update URL query string (tanpa reload page)
const newUrl = `${window.location.pathname}${
newKeyword ? `?keyword=${encodeURIComponent(newKeyword)}` : ""
}`;
window.history.pushState({ path: newUrl }, "", newUrl);
// 3. Update visible keyword text di <em>{{ $keyword }}</em>>
const keywordDisplay = document.querySelector(".qs-header em");
if (keywordDisplay) {
keywordDisplay.textContent = newKeyword || "-";
}
};
this.searchButton.addEventListener("click", handleSearch);
this.keywordInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
handleSearch();
}
});
// Handle input change untuk real-time validation
this.keywordInput.addEventListener("input", (event) => {
const value = event.target.value.trim();
// Remove existing classes
this.keywordInput.classList.remove("valid", "warning", "invalid");
// Jika input kosong atau hanya spasi, show empty state
if (!value || value.length === 0) {
this.showEmptyState("Mulai Pencarian");
return;
}
// Jika kurang dari 3 karakter, show warning
if (value.length < 3) {
this.showEmptyState("Minimal 3 karakter untuk pencarian");
this.keywordInput.classList.add("warning");
return;
}
// Jika valid, add valid class
this.keywordInput.classList.add("valid");
});
// Handle input focus untuk clear warning state
this.keywordInput.addEventListener("focus", () => {
const value = this.keywordInput.value.trim();
if (value.length >= 3) {
// Jika sudah valid, hide empty state
this.emptyState.style.display = "none";
}
});
// Handle input blur untuk final validation
this.keywordInput.addEventListener("blur", () => {
const value = this.keywordInput.value.trim();
if (!value || value.length === 0) {
this.showEmptyState("Mulai Pencarian");
} else if (value.length < 3) {
this.showEmptyState("Minimal 3 karakter untuk pencarian");
}
});
// Handle Escape key untuk clear search
this.keywordInput.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
this.clearSearch();
}
});
}
buildUrl(keyword) {
const url = new URL(this.baseUrl, window.location.origin);
// Validasi keyword tidak kosong dan tidak hanya spasi
if (keyword && keyword.trim() !== "" && keyword.trim().length >= 3) {
url.searchParams.set("search", keyword.trim());
} else {
url.searchParams.delete("search"); // pastikan tidak ada search param
}
return url.toString();
}
showSearchResults() {
this.searchHeader.style.display = "block";
this.tableWrapper.style.display = "block";
this.emptyState.style.display = "none";
}
showEmptyState(message = "Tidak ada data yang ditemukan") {
this.searchHeader.style.display = "none";
this.tableWrapper.style.display = "none";
this.emptyState.style.display = "block";
// Update empty state message and icon
const emptyStateTitle = this.emptyState.querySelector("h4");
const emptyStateDesc = this.emptyState.querySelector("p");
const emptyIcon = this.emptyState.querySelector(".empty-icon i");
if (emptyStateTitle) {
emptyStateTitle.textContent = message;
}
if (emptyStateDesc) {
if (message === "Mulai Pencarian") {
emptyStateDesc.textContent =
"Masukkan kata kunci minimal 3 karakter untuk mencari data PBG";
} else if (message === "Minimal 3 karakter untuk pencarian") {
emptyStateDesc.textContent =
"Masukkan kata kunci minimal 3 karakter untuk mencari data PBG";
} else {
emptyStateDesc.textContent =
"Coba gunakan kata kunci yang berbeda atau lebih spesifik";
}
}
// Update icon based on message
if (emptyIcon) {
if (message === "Mulai Pencarian") {
emptyIcon.className = "fas fa-search fa-3x text-muted";
} else if (message === "Minimal 3 karakter untuk pencarian") {
emptyIcon.className =
"fas fa-exclamation-triangle fa-3x text-warning";
} else {
emptyIcon.className = "fas fa-search fa-3x text-muted";
}
}
// Clear existing table if any
if (this.table) {
this.table.destroy();
this.table = null;
}
}
clearSearch() {
this.keywordInput.value = "";
this.showEmptyState("Mulai Pencarian");
// Reset CSS classes
this.keywordInput.classList.remove("valid", "warning", "invalid");
// Clear URL parameter
const newUrl = window.location.pathname;
window.history.pushState({ path: newUrl }, "", newUrl);
// Reset datatable URL
this.datatableUrl = null;
}
initDatatable() {
const tableContainer = document.getElementById(
"datatable-public-search"
);
const config = {
columns: [
{ name: "ID", width: "80px" },
{ name: "Nama Pemohon", width: "150px" },
{ name: "Nama Pemilik", width: "150px" },
{ name: "Kondisi", width: "120px" },
{ name: "Nomor Registrasi", width: "180px" },
{ name: "Status", width: "120px" },
{ name: "Jenis Fungsi", width: "150px" },
{ name: "Nama Bangunan", width: "200px" },
{ name: "Jenis Konsultasi", width: "150px" },
{ name: "Tanggal Jatuh Tempo", width: "140px" },
{ name: "Retribusi", width: "120px" },
{ name: "Catatan Kekurangan Dokumen", width: "120px" },
],
search: false,
pagination: {
limit: 15,
server: {
url: (prev, page) =>
`${prev}${prev.includes("?") ? "&" : "?"}page=${
page + 1
}`,
},
},
sort: true,
server: {
url: this.datatableUrl,
then: (data) => {
// Check if data is empty
if (!data.data || data.data.length === 0) {
this.showEmptyState(
data.message || "Tidak ada data yang ditemukan"
);
return [];
}
return data.data.map((item) => [
item.id || "-",
item.name || "-",
item.owner_name || "-",
item.condition || "-",
item.registration_number || "-",
item.status_name || "-",
item.function_type || "-",
item.name_building || "-",
item.consultation_type || "-",
item.due_date || "-",
item.nilai_retribusi_bangunan
? addThousandSeparators(
item.nilai_retribusi_bangunan
)
: "-",
item.note || "-",
]);
},
total: (data) => data.total || 0,
error: (error) => {
console.error("Datatable error:", error);
this.showEmptyState(
"Terjadi kesalahan saat mengambil data"
);
},
},
};
if (this.table) {
this.table
.updateConfig({
...config,
server: { ...config.server, url: this.datatableUrl },
})
.forceRender();
} else {
tableContainer.innerHTML = "";
this.table = new Grid(config).render(tableContainer);
}
}
}
document.addEventListener("DOMContentLoaded", function () {
const app = new PublicSearch();
app.init();
});

View File

@@ -1,4 +1,5 @@
import { Grid, html } from "gridjs";
import { addThousandSeparators } from "../global-config";
class QuickSearchResult {
constructor() {
@@ -62,15 +63,19 @@ class QuickSearchResult {
const config = {
columns: [
"ID",
{ name: "Name" },
{ name: "Condition" },
"Registration Number",
"Document Number",
{ name: "Address" },
{ name: "Nama Pemohon" },
{ name: "Nama Pemilik" },
{ name: "Kondisi" },
"Nomor Registrasi",
"Nomor Dokumen",
{ name: "Alamat" },
"Status",
"Function Type",
"Consultation Type",
{ name: "Due Date" },
"Jenis Fungsi",
{ name: "Nama Bangunan" },
"Jenis Konsultasi",
{ name: "Tanggal Jatuh Tempo" },
{ name: "Retribusi" },
{ name: "Catatan Kekurangan Dokumen" },
{
name: "Action",
formatter: (cell) => {
@@ -101,14 +106,18 @@ class QuickSearchResult {
data.data.map((item) => [
item.id,
item.name,
item.owner_name,
item.condition,
item.registration_number,
item.document_number,
item.address,
item.status_name,
item.function_type,
item.name_building,
item.consultation_type,
item.due_date,
addThousandSeparators(item.nilai_retribusi_bangunan),
item.note || "-",
item,
]),
total: (data) => data.total,

View File

@@ -216,7 +216,9 @@ class SyncronizeTask {
})
.then((data) => {
this.toastMessage.innerText =
data.data.message || "Synchronize successfully!";
data?.data?.message ||
data?.message ||
"Synchronize successfully!";
this.toast.show();
// Update the table if it exists

View File

@@ -0,0 +1,281 @@
.qs-wrapper {
padding: 20px;
width: 100%;
max-width: 100%;
margin: 0 auto;
min-height: 100vh;
}
.qs-toolbar {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.qs-search-form {
width: 100%;
.gsp-input {
flex: 1;
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 16px;
transition: all 0.3s ease;
&:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}
&:invalid {
border-color: #dc3545;
box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.1);
}
&::placeholder {
color: #6c757d;
}
// Style untuk input yang valid
&.valid {
border-color: #28a745;
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.1);
}
// Style untuk input yang warning
&.warning {
border-color: #ffc107;
box-shadow: 0 0 0 3px rgba(255, 193, 7, 0.1);
}
}
.gsp-btn {
padding: 12px 24px;
background: #007bff;
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: #0056b3;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
&:active {
transform: translateY(0);
}
}
}
.qs-header {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-left: 4px solid #007bff;
h2 {
color: #2c3e50;
margin-bottom: 8px;
font-weight: 600;
}
p {
color: #6c757d;
margin: 0;
font-size: 16px;
}
}
.qs-table-wrapper {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.qs-empty-state {
background: white;
border-radius: 8px;
padding: 60px 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
.empty-icon {
color: #dee2e6;
i {
opacity: 0.7;
&.text-warning {
color: #ffc107 !important;
}
}
}
h4 {
font-weight: 500;
margin-bottom: 12px;
}
p {
font-size: 16px;
line-height: 1.5;
max-width: 400px;
margin: 0 auto;
}
}
// GridJS customization
.gridjs-wrapper {
border: none !important;
box-shadow: none !important;
width: 100% !important;
max-width: 100% !important;
}
.gridjs-table {
border: none !important;
width: 100% !important;
table-layout: auto !important;
}
// Ensure table cells don't wrap unnecessarily
.gridjs-td {
border-bottom: 1px solid #e9ecef !important;
padding: 16px 12px !important;
vertical-align: middle !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
max-width: 200px !important;
}
// Allow specific columns to wrap if needed
.gridjs-td:nth-child(4), // Kondisi
.gridjs-td:nth-child(7), // Jenis Fungsi
.gridjs-td:nth-child(8), // Nama Bangunan
.gridjs-td:nth-child(9), // Jenis Konsultasi
.gridjs-td:nth-child(10) {
// Tanggal Jatuh Tempo
white-space: normal !important;
word-wrap: break-word !important;
max-width: 150px !important;
}
.gridjs-th {
background: #f8f9fa !important;
border-bottom: 2px solid #dee2e6 !important;
font-weight: 600 !important;
color: #495057 !important;
padding: 16px 12px !important;
}
.gridjs-td {
border-bottom: 1px solid #e9ecef !important;
padding: 16px 12px !important;
vertical-align: middle !important;
}
.gridjs-pagination {
border-top: 1px solid #e9ecef !important;
padding: 20px !important;
.gridjs-pages {
button {
border: 1px solid #dee2e6 !important;
border-radius: 4px !important;
padding: 8px 12px !important;
margin: 0 2px !important;
&:hover {
background: #e9ecef !important;
}
&.gridjs-currentPage {
background: #007bff !important;
color: white !important;
border-color: #007bff !important;
}
}
}
}
// Responsive design
@media (max-width: 768px) {
.qs-wrapper {
padding: 15px;
}
.qs-toolbar {
padding: 15px;
}
.qs-search-form {
flex-direction: column;
gap: 15px;
.gsp-input {
width: 100%;
}
.gsp-btn {
width: 100%;
}
}
.qs-header {
padding: 15px;
h2 {
font-size: 20px;
}
}
.qs-empty-state {
padding: 40px 15px;
.empty-icon i {
font-size: 2.5rem !important;
}
h4 {
font-size: 18px;
}
p {
font-size: 14px;
}
}
}
// Table responsive improvements
@media (max-width: 1200px) {
.gridjs-wrapper {
overflow-x: auto !important;
}
.gridjs-table {
min-width: 1000px !important;
}
}
// Ensure full width on larger screens
@media (min-width: 1201px) {
.qs-wrapper {
padding: 20px 40px;
}
.gridjs-wrapper {
max-width: none !important;
}
}

View File

@@ -2,6 +2,30 @@
color: #000; // black text
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
// Empty state styles
.empty-state {
text-align: center;
padding: 3rem 1rem;
.empty-icon {
font-size: 3rem;
color: #6c757d;
margin-bottom: 1rem;
opacity: 0.6;
}
.empty-title {
font-size: 1.25rem;
color: #6c757d;
margin-bottom: 0.5rem;
}
.empty-text {
color: #6c757d;
opacity: 0.8;
}
}
.card {
background-color: #fff;

View File

@@ -157,7 +157,7 @@
@endcomponent
@component('components.circle', [
'document_title' => 'Berkas Terverifikasi',
'document_title' => 'Berkas Lengkap',
'document_color' => '#5170ff',
'document_type' => 'Berkas',
'document_id' => 'chart-berkas-terverifikasi',
@@ -174,7 +174,7 @@
</div>
@component('components.circle', [
'document_title' => 'Berkas Belum Terverifikasi',
'document_title' => 'Berkas Belum Lengkap',
'document_color' => '#5170ff',
'document_type' => 'Berkas',
'document_id' => 'chart-berkas-belum-terverifikasi',
@@ -192,7 +192,7 @@
@component('components.circle',[
'document_title' => 'Realisasi Terbit PBG',
'document_title' => 'Realisasi PAD PBG',
'document_color' => '#8cc540',
'document_type' => 'Berkas',
'document_id' => 'chart-realisasi-tebit-pbg',
@@ -203,7 +203,7 @@
@endcomponent
@component('components.circle',[
'document_title' => 'Pembayaran Realisasi PBG',
'document_title' => 'Realisasi PAD',
'document_color' => '#0a0099',
'document_type' => 'Berkas',
'document_id' => 'chart-payment-pbg-task',

View File

@@ -17,10 +17,22 @@
<li class="menu-title">Menu</li>
@php
// Menentukan apakah sebuah menu (atau anaknya) aktif berdasarkan request('menu_id')
function isActiveMenu($menu, $currentId) {
if (!$currentId) return false;
if ((string)$menu->id === (string)$currentId) return true;
foreach ($menu->children as $child) {
if (isActiveMenu($child, $currentId)) return true;
}
return false;
}
function renderMenu($menus) {
$currentMenuId = request('menu_id');
foreach ($menus as $menu) {
$collapseId = "sidebar-" . $menu->id;
$hasChildren = $menu->children->count() > 0;
$isActive = isActiveMenu($menu, $currentMenuId);
// Pastikan route tersedia dan boleh ditampilkan
$menuUrl = '#';
@@ -28,14 +40,14 @@
if (Route::has($menu->url)) {
$menuUrl = route($menu->url, ['menu_id' => $menu->id]);
} else {
$menuUrl = $menu->url . '?menu_id=' . $menu->id;
$menuUrl = $menu->url . (strpos($menu->url, '?') !== false ? '&' : '?') . 'menu_id=' . $menu->id;
}
}
echo '<li class="nav-item ' . ($hasChildren ? 'has-children' : '') . '">';
echo '<a class="nav-link ' . ($hasChildren ? 'menu-arrow' : '') . '"
echo '<li class="nav-item ' . ($hasChildren ? 'has-children' : '') . ' ' . ($isActive ? 'active' : '') . '">';
echo '<a class="nav-link ' . ($hasChildren ? 'menu-arrow' : '') . ' ' . ($isActive ? 'active' : '') . '"
href="' . ($hasChildren ? "#$collapseId" : $menuUrl) . '"
' . ($hasChildren ? 'data-bs-toggle="collapse" role="button" aria-expanded="false" aria-controls="' . $collapseId . '"' : '') . '>';
' . ($hasChildren ? 'data-bs-toggle="collapse" role="button" aria-expanded="' . ($isActive ? 'true' : 'false') . '" aria-controls="' . $collapseId . '"' : '') . '>';
// Tampilkan ikon hanya jika tersedia
if (!empty($menu->icon)) {
@@ -48,7 +60,7 @@
echo '</a>';
if ($hasChildren) {
echo '<div class="collapse" id="' . $collapseId . '">
echo '<div class="collapse ' . ($isActive ? 'show' : '') . '" id="' . $collapseId . '">
<ul class="nav sub-navbar-nav">';
renderMenu($menu->children);
echo '</ul></div>';
@@ -72,4 +84,52 @@
@for ($i = 0; $i < 20; $i++)
<div class="shooting-star"></div>
@endfor
</div>
</div>
<style>
/* Sidebar hover/active contrast improvements */
.app-sidebar .nav-link {
transition: background-color .2s ease, color .2s ease;
border-radius: 6px;
}
/* Hover state (dark green theme) */
.app-sidebar .nav-link:hover {
background-color: #eaf7f0; /* light green */
color: #146c43; /* lighter dark green */
}
/* Active state for parents and leaf items (dark green) */
.app-sidebar .nav-item.active > .nav-link,
.app-sidebar .nav-link.active {
background-color: #198754; /* bootstrap success */
color: #ffffff;
font-weight: 600;
}
/* Optional: subtle left border indicator on active */
.app-sidebar .nav-item.active > .nav-link,
.app-sidebar .sub-navbar-nav .nav-link.active {
box-shadow: inset 4px 0 0 0 #146c43;
}
/* Submenu links */
.app-sidebar .sub-navbar-nav .nav-link:hover {
background-color: #f1fbf5;
color: #146c43;
}
.app-sidebar .sub-navbar-nav .nav-link.active {
background-color: #198754;
color: #ffffff;
font-weight: 600;
}
/* Keep icon color in sync */
.app-sidebar .nav-link:hover .nav-icon iconify-icon,
.app-sidebar .nav-item.active > .nav-link .nav-icon iconify-icon,
.app-sidebar .nav-link.active .nav-icon iconify-icon,
.app-sidebar .sub-navbar-nav .nav-link.active .nav-icon iconify-icon {
color: currentColor;
}
</style>

View File

@@ -9,27 +9,9 @@
class="fs-24 align-middle"></iconify-icon>
</button>
</div>
<!-- App Search-->
<!-- <form class="app-search d-none d-md-block me-auto">
<div class="position-relative">
<input type="search" class="form-control" placeholder="admin,widgets..."
autocomplete="off" value="">
<iconify-icon icon="solar:magnifer-outline" class="search-widget-icon"></iconify-icon>
</div>
</form> -->
</div>
<div class="d-flex align-items-center gap-2">
<!-- Theme Color (Light/Dark) -->
{{-- <div class="topbar-item">
<button type="button" class="topbar-button" id="light-dark-mode">
<iconify-icon icon="solar:moon-outline"
class="fs-22 align-middle light-mode"></iconify-icon>
<iconify-icon icon="solar:sun-2-outline"
class="fs-22 align-middle dark-mode"></iconify-icon>
</button>
</div> --}}
<div class="topbar-item">
<a href="{{ route('chatbot.index') }}" class="topbar-button">
@@ -37,118 +19,6 @@
</a>
</div>
<!-- Notification -->
{{-- <div class="dropdown topbar-item">
<button type="button" class="topbar-button position-relative"
id="page-header-notifications-dropdown" data-bs-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
<iconify-icon icon="solar:bell-bing-outline" class="fs-22 align-middle"></iconify-icon>
<span
class="position-absolute topbar-badge fs-10 translate-middle badge bg-danger rounded-pill">5<span
class="visually-hidden">unread messages</span></span>
</button>
<div class="dropdown-menu py-0 dropdown-lg dropdown-menu-end"
aria-labelledby="page-header-notifications-dropdown">
<div class="p-2 border-bottom bg-light bg-opacity-50">
<div class="row align-items-center">
<div class="col">
<h6 class="m-0 fs-16 fw-semibold"> Notifications (5)</h6>
</div>
<div class="col-auto">
<a href="javascript: void(0);" class="text-dark text-decoration-underline">
<small>Clear All</small>
</a>
</div>
</div>
</div>
<div data-simplebar style="max-height: 250px;">
<!-- Item -->
<a href="javascript:void(0);" class="dropdown-item p-2 border-bottom text-wrap">
<div class="d-flex">
<div class="flex-shrink-0">
<img src="/images/users/avatar-1.jpg"
class="img-fluid me-2 avatar-sm rounded-circle" alt="avatar-1" />
</div>
<div class="flex-grow-1">
<p class="mb-0"><span class="fw-medium">Sally Bieber </span>started
following you. Check out their profile!"</span></p>
</div>
</div>
</a>
<!-- Item -->
<a href="javascript:void(0);" class="dropdown-item p-2 border-bottom">
<div class="d-flex">
<div class="flex-shrink-0">
<div class="avatar-sm me-2">
<span
class="avatar-title text-bg-info fw-semibold fs-20 rounded-circle">
G
</span>
</div>
</div>
<div class="flex-grow-1">
<p class="mb-0 fw-medium">Gloria Chambers</p>
<p class="mb-0 text-wrap">
mentioned you in a comment: '@admin, check this out!
</p>
</div>
</div>
</a>
<!-- Item -->
<a href="javascript:void(0);" class="dropdown-item p-2 border-bottom">
<div class="d-flex">
<div class="flex-shrink-0">
<img src="/images/users/avatar-3.jpg"
class="img-fluid me-2 avatar-sm rounded-circle" alt="avatar-3" />
</div>
<div class="flex-grow-1">
<p class="mb-0 fw-medium">Jacob Gines</p>
<p class="mb-0 text-wrap">
Answered to your comment on the cash flow forecast's graph 🔔.
</p>
</div>
</div>
</a>
<!-- Item -->
<a href="javascript:void(0);" class="dropdown-item p-2 border-bottom">
<div class="d-flex">
<div class="flex-shrink-0">
<div class="avatar-sm me-2">
<span
class="avatar-title bg-soft-warning text-warning fs-20 rounded-circle">
<iconify-icon icon="solar:leaf-outline"></iconify-icon>
</span>
</div>
</div>
<div class="flex-grow-1">
<p class="mb-0 fw-medium text-wrap">A new system update is available.
Update now for the latest features.</p>
</div>
</div>
</a>
<!-- Item -->
<a href="javascript:void(0);" class="dropdown-item p-2 border-bottom">
<div class="d-flex">
<div class="flex-shrink-0">
<img src="/images/users/avatar-5.jpg"
class="img-fluid me-2 avatar-sm rounded-circle" alt="avatar-5" />
</div>
<div class="flex-grow-1">
<p class="mb-0 fw-medium">Shawn Bunch</p>
<p class="mb-0 text-wrap">
commented on your post: 'Great photo!
</p>
</div>
</div>
</a>
</div>
<div class="text-center p-2">
<a href="javascript:void(0);" class="btn btn-primary btn-sm">View All Notification <i
class="bx bx-right-arrow-alt ms-1"></i></a>
</div>
</div>
</div> --}}
<!-- User -->
<div class="dropdown topbar-item">
<a type="button" class="topbar-button" id="page-header-user-dropdown" data-bs-toggle="dropdown"
@@ -162,28 +32,6 @@
<!-- item-->
<h6 class="dropdown-header">{{ Auth::user()->email }}</h6>
<!-- <a class="dropdown-item" href="#">
<iconify-icon icon="solar:user-outline"
class="align-middle me-2 fs-18"></iconify-icon><span class="align-middle">My
Account</span>
</a>
<a class="dropdown-item" href="#">
<iconify-icon icon="solar:wallet-outline"
class="align-middle me-2 fs-18"></iconify-icon><span
class="align-middle">Pricing</span>
</a>
<a class="dropdown-item" href="#">
<iconify-icon icon="solar:help-outline"
class="align-middle me-2 fs-18"></iconify-icon><span
class="align-middle">Help</span>
</a>
<a class="dropdown-item" href="auth-{{ route ('dashboard.home') }}">
<iconify-icon icon="solar:lock-keyhole-outline"
class="align-middle me-2 fs-18"></iconify-icon><span class="align-middle">Lock
screen</span>
</a> -->
<div class="dropdown-divider my-1"></div>
<form id="logout-form" action="{{route('logout')}}" method="POST" style="display: none;">
@@ -200,12 +48,60 @@
</div>
</div>
</header>
<style>
/* Tampilkan hover submenu HANYA saat sidebar collapsed (berbagai kemungkinan class) */
body.sidebar-collapsed .app-sidebar .navbar-nav > li.nav-item.has-children:hover > .collapse,
.app-sidebar.collapsed .navbar-nav > li.nav-item.has-children:hover > .collapse,
.app-sidebar.mini .navbar-nav > li.nav-item.has-children:hover > .collapse {
display: block !important;
position: absolute;
top: 0;
left: calc(100% + 8px);
background: #ffffff;
border-radius: 8px;
box-shadow: 0 8px 20px rgba(0,0,0,0.08);
padding: 6px 6px;
min-width: 260px;
width: clamp(260px, 40vw, 380px); /* responsive, bounded width */
box-sizing: border-box;
overflow: hidden; /* clip inner overflow to maintain box */
z-index: 3;
}
.app-sidebar .sub-navbar-nav {
width: 100%;
max-width: 100%; /* pastikan nggak lebih besar dari box */
}
.app-sidebar .sub-navbar-nav .nav-link,
.app-sidebar .sub-navbar-nav .nav-link .nav-text {
display: block !important;
width: 100% !important;
max-width: 100% !important;
white-space: normal !important;
word-break: break-word !important;
overflow-wrap: break-word !important;
}
.app-sidebar .sub-navbar-nav .nav-link:hover {
background: #f1fbf5;
color: #146c43;
}
.app-sidebar .sub-navbar-nav .nav-link .nav-text {
display: inline !important;
visibility: visible !important;
opacity: 1 !important;
}
/* Do not force-hide parent nav-text in any state */
</style>
<script>
function logoutUser() {
// Hapus token dari localStorage
localStorage.removeItem('token');
// Submit form logout Laravel
document.getElementById('logout-form').submit();
}
</script>
</script>

View File

@@ -112,6 +112,23 @@
</div>
</div>
</div>
@if ($data->pbg_status)
<div class="col-12">
<div class="card">
<div class="card-body">
<h5 class="card-title">Keterangan</h5>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="note" class="form-label">Note</label>
<p class="form-control-plaintext mb-0">{{$data->pbg_status->note}}</p>
</div>
</div>
</div>
</div>
</div>
</div>
@endif
<div class="col-12">
<div class="card">
<div class="card-header">
@@ -208,10 +225,10 @@
</div>
</div>
@else
<div class="row">
<div class="col-md-12">
Data Not Available
</div>
<div class="empty-state">
<i class="fas fa-folder-open empty-icon"></i>
<h5 class="empty-title">Data Tidak Tersedia</h5>
<p class="empty-text">Tidak ada data yang terkait dengan PBG task ini.</p>
</div>
@endif
</div>
@@ -250,10 +267,10 @@
</div>
</div>
@else
<div class="row">
<div class="col-md-12">
Data Not Available
</div>
<div class="empty-state">
<i class="fas fa-folder-open empty-icon"></i>
<h5 class="empty-title">Data Tidak Tersedia</h5>
<p class="empty-text">Tidak ada data yang terkait dengan PBG task ini.</p>
</div>
@endif
</div>
@@ -293,10 +310,10 @@
</div>
@endforeach
@else
<div class="row">
<div class="col-md-12">
Data Not Available
</div>
<div class="empty-state">
<i class="fas fa-folder-open empty-icon"></i>
<h5 class="empty-title">Data Tidak Tersedia</h5>
<p class="empty-text">Tidak ada data yang terkait dengan PBG task ini.</p>
</div>
@endif
</div>
@@ -411,10 +428,10 @@
</div>
</div>
@else
<div class="row">
<div class="col-md-12">
Data Not Available
</div>
<div class="empty-state">
<i class="fas fa-folder-open empty-icon"></i>
<h5 class="empty-title">Data Tidak Tersedia</h5>
<p class="empty-text">Tidak ada data yang terkait dengan PBG task ini.</p>
</div>
@endif
</div>
@@ -506,8 +523,8 @@
@else
<div class="empty-state">
<i class="fas fa-folder-open empty-icon"></i>
<h5 class="empty-title">No Data Lists Available</h5>
<p class="empty-text">There are no data lists associated with this PBG task.</p>
<h5 class="empty-title">Data Tidak Tersedia</h5>
<p class="empty-text">Tidak ada data yang terkait dengan PBG task ini.</p>
</div>
@endif
</div>

View File

@@ -0,0 +1,47 @@
@extends('layouts.base', ['subtitle' => 'Quick Search'])
@section('css')
@vite(['resources/scss/pages/public-search/index.scss'])
@vite(['node_modules/gridjs/dist/theme/mermaid.min.css'])
@endsection
@section('content')
<input type="hidden" value="{{ route('public-search-datatable') }}" id="base_url_datatable" />
<div class="qs-wrapper">
<div class="qs-toolbar d-flex justify-content-between align-items-center pt-4 pb-4">
<!-- Search Area (no form action) -->
<div class="qs-search-form d-flex align-items-center">
<input
type="text"
id="search_input"
class="gsp-input me-2"
placeholder="Cari data..."
required
/>
<button type="button" id="search_button" class="gsp-btn">Cari</button>
</div>
</div>
<div class="qs-header mb-3" id="search-header" style="display: none;">
<h2>Hasil Pencarian</h2>
<p>Berikut adalah data hasil pencarian berdasarkan kata kunci yang Anda masukkan.</p>
</div>
<div class="qs-table-wrapper" id="table-wrapper" style="display: none;">
<div class="p-3" id="datatable-public-search"></div>
</div>
<div class="qs-empty-state text-center py-5" id="empty-state">
<div class="empty-icon mb-3">
<i class="fas fa-search fa-3x text-muted"></i>
</div>
<h4 class="text-muted mb-2">Mulai Pencarian</h4>
<p class="text-muted">Masukkan kata kunci minimal 3 karakter untuk mencari data PBG</p>
</div>
</div>
@endsection
@section('scripts')
@vite(['resources/js/public-search/index.js'])
@endsection

View File

@@ -65,6 +65,23 @@
</div>
</div>
</div>
@if ($data->pbg_status)
<div class="col-12">
<div class="card">
<div class="card-body">
<h5 class="card-title text-black">Catatan Kekurangan Dokumen</h5>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="note" class="form-label text-black">Keterangan</label>
<p class="form-control-plaintext mb-0 text-black">{{$data->pbg_status->note}}</p>
</div>
</div>
</div>
</div>
</div>
</div>
@endif
<div class="col-12">
<div class="card">
<div class="card-header">
@@ -124,7 +141,8 @@
<div class="col-md-6">
<dl class="row mb-0">
<dt class="col-sm-4">Nilai Retribusi Bangunan</dt>
<dd class="col-sm-8">{{$data->pbg_task_retributions->nilai_retribusi_bangunan ?? '-'}}</dd>
<dd class="col-sm-8">{{ number_format($data->pbg_task_retributions->nilai_retribusi_bangunan, 2, ',', '.') }}</dd>
<dt class="col-sm-4">Nilai Prasarana</dt>
<dd class="col-sm-8">{{$data->pbg_task_retributions->nilai_prasarana ?? '-'}}</dd>
@@ -136,13 +154,15 @@
<dd class="col-sm-8">{{$data->pbg_task_retributions->underpayment ?? '-'}}</dd>
<dt class="col-sm-4">SKRD Amount</dt>
<dd class="col-sm-8">{{$data->pbg_task_retributions->skrd_amount ?? '-'}}</dd>
<dd class="col-sm-8">{{ number_format($data->pbg_task_retributions->skrd_amount, 2, ',', '.') }}</dd>
</dl>
</div>
</div>
@else
<div class="alert alert-secondary" role="alert">
Data Not Available
<div class="empty-state">
<i class="fas fa-folder-open empty-icon"></i>
<h5 class="empty-title">Data Tidak Tersedia</h5>
<p class="empty-text">Tidak ada data yang terkait dengan PBG task ini.</p>
</div>
@endif
</div>
@@ -171,8 +191,10 @@
<dd class="col-sm-8">{{$data->pbg_task_index_integrations->total ?? '-'}}</dd>
</dl>
@else
<div class="alert alert-secondary" role="alert">
Data Not Available
<div class="empty-state">
<i class="fas fa-folder-open empty-icon"></i>
<h5 class="empty-title">Data Tidak Tersedia</h5>
<p class="empty-text">Tidak ada data yang terkait dengan PBG task ini.</p>
</div>
@endif
</div>
@@ -206,8 +228,10 @@
</div>
@endforeach
@else
<div class="alert alert-secondary" role="alert">
Data Not Available
<div class="empty-state">
<i class="fas fa-folder-open empty-icon"></i>
<h5 class="empty-title">Data Tidak Tersedia</h5>
<p class="empty-text">Tidak ada data yang terkait dengan PBG task ini.</p>
</div>
@endif
</div>

View File

@@ -79,7 +79,6 @@ Route::group(['middleware' => 'auth:sanctum'], function (){
Route::get('/scraping','index')->name('scraping');
Route::get('/retry-scraping/{id}','retry_syncjob')->name('retry-scraping');
});
// Route::apiResource('/scraping', ScrapingController::class);
// reklame
Route::apiResource('advertisements', AdvertisementController::class);
@@ -118,11 +117,6 @@ Route::group(['middleware' => 'auth:sanctum'], function (){
// sync pbg google sheet
Route::apiResource('/api-google-sheet', GoogleSheetController::class);
Route::get('/sync-task', [SyncronizeController::class, 'syncPbgTask'])->name('api.task');
Route::get('/get-user-token', [SyncronizeController::class, 'getUserToken'])->name('api.task.token');
Route::get('/get-index-integration-retribution/{uuid}', [SyncronizeController::class, 'syncIndexIntegration'])->name('api.task.inntegration');
Route::get('/sync-task-submit/{uuid}', [SyncronizeController::class, 'syncTaskDetailSubmit'])->name('api.task.submit');
Route::get('/sync-task-assignments/{uuid}', [SyncronizeController::class, 'syncTaskAssignments'])->name('api.task.assignments');
// menus api
Route::controller(MenusController::class)->group(function (){

View File

@@ -8,6 +8,5 @@ Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote')->hourly();
Schedule::command("app:scraping-leader-data")->dailyAt("00:00");
Schedule::command("app:start-scraping-data --confirm")->dailyAt("00:30");
Schedule::command("app:start-scraping-data --confirm")->dailyAt("00:00");

View File

@@ -38,8 +38,10 @@ use Illuminate\Support\Facades\Route;
require __DIR__ . '/auth.php';
Route::get('/search', [QuickSearchController::class, 'index'])->name('search');
Route::get('/public-search', [QuickSearchController::class, 'public_search'])->name('public-search');
Route::get('/search-result', [QuickSearchController::class, 'search_result'])->name('search-result');
Route::get('/quick-search-datatable', [QuickSearchController::class, 'quick_search_datatable'])->name('quick-search-datatable');
Route::get('/public-search-datatable', [QuickSearchController::class, 'public_search_datatable'])->name('public-search-datatable');
Route::get('/quick-search/{id}', [QuickSearchController::class, 'show'])->name('quick-search.detail');
Route::get('/quick-search/{uuid}/task-assignments', [QuickSearchController::class, 'task_assignments'])->name('api.quick-search-task-assignments');

View File

@@ -53,6 +53,7 @@ export default defineConfig(({ mode }) => {
"resources/scss/pages/quick-search/detail.scss",
"resources/scss/pages/quick-search/index.scss",
"resources/scss/pages/quick-search/result.scss",
"resources/scss/pages/public-search/index.scss",
"resources/scss/pages/pbg-task/show.scss",
"node_modules/quill/dist/quill.snow.css",
@@ -148,6 +149,8 @@ export default defineConfig(({ mode }) => {
"resources/js/quick-search/index.js",
"resources/js/quick-search/result.js",
"resources/js/quick-search/detail.js",
// public-search
"resources/js/public-search/index.js",
// growth-report
"resources/js/report/growth-report/index.js",
// dummy