Compare commits

...

6 Commits

Author SHA1 Message Date
arifal hidayat
91085e8796 fix duplicate insert when chunk 2025-09-14 20:25:36 +07:00
arifal
61e6eb9803 fix delete unused service 2025-09-12 17:31:18 +07:00
arifal
148dfebb4a fix relation name 2025-09-12 14:39:42 +07:00
arifal
aa34fff979 fix sidebar hover on collapsed 2025-09-12 14:14:32 +07:00
arifal
1a24b18719 fix collapse sidebar when active 2025-09-12 13:06:22 +07:00
arifal
e265e2ec35 fix sort order sidebar menu and percentage bigdata resume 2025-09-12 11:22:18 +07:00
19 changed files with 157 additions and 1211 deletions

View File

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

View File

@@ -76,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
@@ -146,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,

View File

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

View File

@@ -335,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);
});
});
@@ -530,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);
});
});
@@ -554,10 +554,10 @@ class RequestAssignmentController extends Controller
->whereIn("status", PbgTaskStatus::getNonVerified())
// Additional condition: unit IS NULL OR unit <= 1
->where(function ($q3) {
$q3->whereDoesntHave('pbg_task_details', function ($q4) {
$q3->whereDoesntHave('pbg_task_detail', function ($q4) {
$q4->where('unit', '>', 1);
})
->orWhereDoesntHave('pbg_task_details');
->orWhereDoesntHave('pbg_task_detail');
});
})
->where('is_valid', true)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -688,21 +688,27 @@ class ServiceGoogleSheet
// Build and insert in small batches to avoid high memory usage
$batch = [];
$batchSize = 500;
$inserted = 0;
// Stream rows in chunks from API to avoid loading full sheet
$rowStart = 2; // data starts from row 2
$chunkRowSize = 800; // number of rows per chunk
$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) {
@@ -838,11 +844,6 @@ class ServiceGoogleSheet
if (function_exists('gc_collect_cycles')) { gc_collect_cycles(); }
}
if (!empty($batch)) {
\App\Models\PbgTaskPayment::insert($batch);
$inserted += count($batch);
}
Log::info('PBG Task Payments reloaded from sheet', ['inserted' => $inserted]);
return ['success' => true, 'inserted' => $inserted];

View File

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

View File

@@ -246,8 +246,6 @@ class ServiceTabPbgTask
$data = $responseData['data'];
Log::info("Executed uid : {$uuid}");
// Use the static method from PbgTaskDetail model to create/update
PbgStatus::createOrUpdateFromApi($data, $uuid);

View File

@@ -21,7 +21,7 @@ class UsersRoleMenuSeeder extends Seeder
// Fetch all menus in a single query and index by name
$menus = Menu::whereIn('name', [
'Dashboard', 'Master', 'Settings', 'Data Settings', 'Data', 'Laporan', 'Neng Bedas',
'Approval', 'Tools', 'Dashboard 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 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 PBG', 'Users', 'Syncronize',
'Approval', 'Tools', 'Users', 'Syncronize', 'Dashboard Pimpinan (SIMBG)',
'Menu', 'Role', 'Setting Dashboard', 'PBG', 'Reklame', 'Usaha atau Industri', 'Pariwisata',
'Lap Pariwisata', 'UMKM', 'Dashboard Potensi', 'Tata Ruang', 'PDAM', 'Dalam Sistem',
'Luar Sistem', 'Lap Pimpinan', 'Google Sheets', 'TPA TPT', 'Approval Pejabat',

View File

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

View File

@@ -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>';
@@ -73,3 +85,51 @@
<div class="shooting-star"></div>
@endfor
</div>
<style>
/* Sidebar hover/active contrast improvements */
.app-sidebar .nav-link {
transition: background-color .2s ease, color .2s ease;
border-radius: 6px;
}
/* Hover state (dark green theme) */
.app-sidebar .nav-link:hover {
background-color: #eaf7f0; /* light green */
color: #146c43; /* lighter dark green */
}
/* Active state for parents and leaf items (dark green) */
.app-sidebar .nav-item.active > .nav-link,
.app-sidebar .nav-link.active {
background-color: #198754; /* bootstrap success */
color: #ffffff;
font-weight: 600;
}
/* Optional: subtle left border indicator on active */
.app-sidebar .nav-item.active > .nav-link,
.app-sidebar .sub-navbar-nav .nav-link.active {
box-shadow: inset 4px 0 0 0 #146c43;
}
/* Submenu links */
.app-sidebar .sub-navbar-nav .nav-link:hover {
background-color: #f1fbf5;
color: #146c43;
}
.app-sidebar .sub-navbar-nav .nav-link.active {
background-color: #198754;
color: #ffffff;
font-weight: 600;
}
/* Keep icon color in sync */
.app-sidebar .nav-link:hover .nav-icon iconify-icon,
.app-sidebar .nav-item.active > .nav-link .nav-icon iconify-icon,
.app-sidebar .nav-link.active .nav-icon iconify-icon,
.app-sidebar .sub-navbar-nav .nav-link.active .nav-icon iconify-icon {
color: currentColor;
}
</style>

View File

@@ -9,27 +9,9 @@
class="fs-24 align-middle"></iconify-icon>
</button>
</div>
<!-- App Search-->
<!-- <form class="app-search d-none d-md-block me-auto">
<div class="position-relative">
<input type="search" class="form-control" placeholder="admin,widgets..."
autocomplete="off" value="">
<iconify-icon icon="solar:magnifer-outline" class="search-widget-icon"></iconify-icon>
</div>
</form> -->
</div>
<div class="d-flex align-items-center gap-2">
<!-- Theme Color (Light/Dark) -->
{{-- <div class="topbar-item">
<button type="button" class="topbar-button" id="light-dark-mode">
<iconify-icon icon="solar:moon-outline"
class="fs-22 align-middle light-mode"></iconify-icon>
<iconify-icon icon="solar:sun-2-outline"
class="fs-22 align-middle dark-mode"></iconify-icon>
</button>
</div> --}}
<div class="topbar-item">
<a href="{{ route('chatbot.index') }}" class="topbar-button">
@@ -37,118 +19,6 @@
</a>
</div>
<!-- Notification -->
{{-- <div class="dropdown topbar-item">
<button type="button" class="topbar-button position-relative"
id="page-header-notifications-dropdown" data-bs-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
<iconify-icon icon="solar:bell-bing-outline" class="fs-22 align-middle"></iconify-icon>
<span
class="position-absolute topbar-badge fs-10 translate-middle badge bg-danger rounded-pill">5<span
class="visually-hidden">unread messages</span></span>
</button>
<div class="dropdown-menu py-0 dropdown-lg dropdown-menu-end"
aria-labelledby="page-header-notifications-dropdown">
<div class="p-2 border-bottom bg-light bg-opacity-50">
<div class="row align-items-center">
<div class="col">
<h6 class="m-0 fs-16 fw-semibold"> Notifications (5)</h6>
</div>
<div class="col-auto">
<a href="javascript: void(0);" class="text-dark text-decoration-underline">
<small>Clear All</small>
</a>
</div>
</div>
</div>
<div data-simplebar style="max-height: 250px;">
<!-- Item -->
<a href="javascript:void(0);" class="dropdown-item p-2 border-bottom text-wrap">
<div class="d-flex">
<div class="flex-shrink-0">
<img src="/images/users/avatar-1.jpg"
class="img-fluid me-2 avatar-sm rounded-circle" alt="avatar-1" />
</div>
<div class="flex-grow-1">
<p class="mb-0"><span class="fw-medium">Sally Bieber </span>started
following you. Check out their profile!"</span></p>
</div>
</div>
</a>
<!-- Item -->
<a href="javascript:void(0);" class="dropdown-item p-2 border-bottom">
<div class="d-flex">
<div class="flex-shrink-0">
<div class="avatar-sm me-2">
<span
class="avatar-title text-bg-info fw-semibold fs-20 rounded-circle">
G
</span>
</div>
</div>
<div class="flex-grow-1">
<p class="mb-0 fw-medium">Gloria Chambers</p>
<p class="mb-0 text-wrap">
mentioned you in a comment: '@admin, check this out!
</p>
</div>
</div>
</a>
<!-- Item -->
<a href="javascript:void(0);" class="dropdown-item p-2 border-bottom">
<div class="d-flex">
<div class="flex-shrink-0">
<img src="/images/users/avatar-3.jpg"
class="img-fluid me-2 avatar-sm rounded-circle" alt="avatar-3" />
</div>
<div class="flex-grow-1">
<p class="mb-0 fw-medium">Jacob Gines</p>
<p class="mb-0 text-wrap">
Answered to your comment on the cash flow forecast's graph 🔔.
</p>
</div>
</div>
</a>
<!-- Item -->
<a href="javascript:void(0);" class="dropdown-item p-2 border-bottom">
<div class="d-flex">
<div class="flex-shrink-0">
<div class="avatar-sm me-2">
<span
class="avatar-title bg-soft-warning text-warning fs-20 rounded-circle">
<iconify-icon icon="solar:leaf-outline"></iconify-icon>
</span>
</div>
</div>
<div class="flex-grow-1">
<p class="mb-0 fw-medium text-wrap">A new system update is available.
Update now for the latest features.</p>
</div>
</div>
</a>
<!-- Item -->
<a href="javascript:void(0);" class="dropdown-item p-2 border-bottom">
<div class="d-flex">
<div class="flex-shrink-0">
<img src="/images/users/avatar-5.jpg"
class="img-fluid me-2 avatar-sm rounded-circle" alt="avatar-5" />
</div>
<div class="flex-grow-1">
<p class="mb-0 fw-medium">Shawn Bunch</p>
<p class="mb-0 text-wrap">
commented on your post: 'Great photo!
</p>
</div>
</div>
</a>
</div>
<div class="text-center p-2">
<a href="javascript:void(0);" class="btn btn-primary btn-sm">View All Notification <i
class="bx bx-right-arrow-alt ms-1"></i></a>
</div>
</div>
</div> --}}
<!-- User -->
<div class="dropdown topbar-item">
<a type="button" class="topbar-button" id="page-header-user-dropdown" data-bs-toggle="dropdown"
@@ -162,28 +32,6 @@
<!-- item-->
<h6 class="dropdown-header">{{ Auth::user()->email }}</h6>
<!-- <a class="dropdown-item" href="#">
<iconify-icon icon="solar:user-outline"
class="align-middle me-2 fs-18"></iconify-icon><span class="align-middle">My
Account</span>
</a>
<a class="dropdown-item" href="#">
<iconify-icon icon="solar:wallet-outline"
class="align-middle me-2 fs-18"></iconify-icon><span
class="align-middle">Pricing</span>
</a>
<a class="dropdown-item" href="#">
<iconify-icon icon="solar:help-outline"
class="align-middle me-2 fs-18"></iconify-icon><span
class="align-middle">Help</span>
</a>
<a class="dropdown-item" href="auth-{{ route ('dashboard.home') }}">
<iconify-icon icon="solar:lock-keyhole-outline"
class="align-middle me-2 fs-18"></iconify-icon><span class="align-middle">Lock
screen</span>
</a> -->
<div class="dropdown-divider my-1"></div>
<form id="logout-form" action="{{route('logout')}}" method="POST" style="display: none;">
@@ -200,6 +48,54 @@
</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
@@ -208,4 +104,4 @@
// Submit form logout Laravel
document.getElementById('logout-form').submit();
}
</script>
</script>

View File

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

View File

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