Compare commits
23 Commits
fef6ae7522
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91085e8796 | ||
|
|
61e6eb9803 | ||
|
|
148dfebb4a | ||
|
|
aa34fff979 | ||
|
|
1a24b18719 | ||
|
|
e265e2ec35 | ||
|
|
e577da737b | ||
|
|
05ca927c38 | ||
|
|
fc4b419878 | ||
|
|
53d12d6798 | ||
|
|
809eb85255 | ||
|
|
8a513460bb | ||
|
|
fc74875cce | ||
|
|
beb7d935c9 | ||
|
|
5c4cebd2b3 | ||
|
|
cbe3d00c96 | ||
|
|
65d9247b46 | ||
|
|
63310f2748 | ||
|
|
c6257b79bf | ||
|
|
38493063c4 | ||
|
|
954b2d8716 | ||
|
|
41cfce589b | ||
|
|
8de1b51fea |
@@ -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
3
.gitignore
vendored
@@ -21,3 +21,6 @@ yarn-error.log
|
||||
/.nova
|
||||
/.vscode
|
||||
/.zed
|
||||
/.composer
|
||||
/.config
|
||||
/.npm
|
||||
@@ -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 [31mmajor[39m version of npm available! [31m10.8.2[39m -> [34m11.5.2[39m
|
||||
16 notice Changelog: [34mhttps://github.com/npm/cli/releases/tag/v11.5.2[39m
|
||||
16 notice To update run: [4mnpm install -g npm@11.5.2[24m
|
||||
16 notice { force: true, [Symbol(proc-log.meta)]: true }
|
||||
17 verbose exit 0
|
||||
18 info ok
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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.');
|
||||
|
||||
|
||||
118
app/Exports/PbgTaskExport.php
Normal file
118
app/Exports/PbgTaskExport.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use App\Models\PbgTask;
|
||||
use App\Enums\PbgTaskFilterData;
|
||||
|
||||
class PbgTaskExport implements FromCollection, WithHeadings
|
||||
{
|
||||
protected $category;
|
||||
protected $year;
|
||||
|
||||
public function __construct(string $category, int $year)
|
||||
{
|
||||
$this->category = $category;
|
||||
$this->year = $year;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public function collection()
|
||||
{
|
||||
$query = PbgTask::query()
|
||||
->whereYear('task_created_at', $this->year);
|
||||
|
||||
// Menggunakan switch case karena lebih readable dan maintainable
|
||||
// untuk multiple conditions yang berbeda
|
||||
switch ($this->category) {
|
||||
case PbgTaskFilterData::all->value:
|
||||
// Tidak ada filter tambahan, ambil semua data
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::business->value:
|
||||
$query->where('application_type', 'business');
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::non_business->value:
|
||||
$query->where('application_type', 'non-business');
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::verified->value:
|
||||
$query->where('is_valid', true);
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::non_verified->value:
|
||||
$query->where('is_valid', false);
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::potention->value:
|
||||
$query->where('status', 'potention');
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::issuance_realization_pbg->value:
|
||||
$query->where('status', 'issuance-realization-pbg');
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::process_in_technical_office->value:
|
||||
$query->where('status', 'process-in-technical-office');
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::waiting_click_dpmptsp->value:
|
||||
$query->where('status', 'waiting-click-dpmptsp');
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::non_business_rab->value:
|
||||
$query->where('application_type', 'non-business')
|
||||
->where('consultation_type', 'rab');
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::non_business_krk->value:
|
||||
$query->where('application_type', 'non-business')
|
||||
->where('consultation_type', 'krk');
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::business_rab->value:
|
||||
$query->where('application_type', 'business')
|
||||
->where('consultation_type', 'rab');
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::business_krk->value:
|
||||
$query->where('application_type', 'business')
|
||||
->where('consultation_type', 'krk');
|
||||
break;
|
||||
|
||||
case PbgTaskFilterData::business_dlh->value:
|
||||
$query->where('application_type', 'business')
|
||||
->where('consultation_type', 'dlh');
|
||||
break;
|
||||
|
||||
default:
|
||||
// Jika category tidak dikenali, return empty collection
|
||||
return collect();
|
||||
}
|
||||
|
||||
return $query->select([
|
||||
'registration_number',
|
||||
'document_number',
|
||||
'owner_name',
|
||||
'address',
|
||||
'name as building_name',
|
||||
'function_type'
|
||||
])->get();
|
||||
}
|
||||
|
||||
public function headings(): array{
|
||||
return [
|
||||
'Nomor Registrasi',
|
||||
'Nomor Dokumen',
|
||||
'Nama Pemilik',
|
||||
'Alamat Pemilik',
|
||||
'Nama Bangunan',
|
||||
'Fungsi Bangunan',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
57
app/Models/PbgStatus.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Carbon\Carbon;
|
||||
class PbgStatus extends Model
|
||||
{
|
||||
protected $table = 'pbg_statuses';
|
||||
|
||||
protected $fillable = [
|
||||
'pbg_task_uuid',
|
||||
'status',
|
||||
'status_name',
|
||||
'slf_status',
|
||||
'slf_status_name',
|
||||
'due_date',
|
||||
'uid',
|
||||
'note',
|
||||
'file',
|
||||
'data_due_date',
|
||||
'data_created_at',
|
||||
'slf_data',
|
||||
];
|
||||
|
||||
public function pbgTask()
|
||||
{
|
||||
return $this->belongsTo(PbgTask::class, 'pbg_task_uuid', 'uuid');
|
||||
}
|
||||
|
||||
public static function createOrUpdateFromApi(array $apiResponse, string $pbgTaskUuid)
|
||||
{
|
||||
$data = $apiResponse['data'] ?? [];
|
||||
|
||||
return self::updateOrCreate(
|
||||
[
|
||||
'pbg_task_uuid' => $pbgTaskUuid,
|
||||
'status' => $apiResponse['status'], // key pencarian unik
|
||||
],
|
||||
[
|
||||
'status_name' => $apiResponse['status_name'] ?? null,
|
||||
'slf_status' => $apiResponse['slf_status'] ?? null,
|
||||
'slf_status_name' => $apiResponse['slf_status_name'] ?? null,
|
||||
'due_date' => $apiResponse['due_date'] ?? null,
|
||||
|
||||
// nested data
|
||||
'uid' => $data['uid'] ?? null,
|
||||
'note' => $data['note'] ?? null,
|
||||
'file' => $data['file'] ?? null,
|
||||
'data_due_date' => $data['due_date'] ?? null,
|
||||
'data_created_at' => isset($data['created_at']) ? Carbon::parse($data['created_at'])->format('Y-m-d H:i:s') : null,
|
||||
|
||||
'slf_data' => $apiResponse['slf_data'] ?? null,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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'
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 : [];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()]);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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){
|
||||
|
||||
@@ -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',
|
||||
|
||||
1
docker/php/memory-limit.ini
Normal file
1
docker/php/memory-limit.ini
Normal file
@@ -0,0 +1 @@
|
||||
memory_limit=512M
|
||||
@@ -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()
|
||||
)}`;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
312
resources/js/public-search/index.js
Normal file
312
resources/js/public-search/index.js
Normal 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();
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
281
resources/scss/pages/public-search/index.scss
Normal file
281
resources/scss/pages/public-search/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
47
resources/views/public-search/index.blade.php
Normal file
47
resources/views/public-search/index.blade.php
Normal 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
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (){
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user