fix handle token login when loop and fix width column on task fix color default danger success scss, fix add timeout on php.ini, add scraping for execute from api, add check api for handle disabled button sync

This commit is contained in:
arifal
2025-01-31 18:01:27 +07:00
parent d70fefb12d
commit dae0cd481d
15 changed files with 233 additions and 90 deletions

View File

@@ -2,13 +2,17 @@
namespace App\Http\Controllers\Api;
use App\Enums\ImportDatasourceStatus;
use App\Http\Controllers\Controller;
use App\Http\Resources\ImportDatasourceResource;
use App\Models\ImportDatasource;
use App\Traits\GlobalApiResponse;
use Exception;
use Illuminate\Http\Request;
class ImportDatasourceController extends Controller
{
use GlobalApiResponse;
/**
* Display a listing of the resource.
*/
@@ -23,6 +27,19 @@ class ImportDatasourceController extends Controller
return ImportDatasourceResource::collection($query->paginate());
}
public function checkImportDatasource(){
try{
$data = ImportDatasource::where("status",ImportDatasourceStatus::Processing->value )->count();
$result = [
"can_execute" => $data === 0,
"total_processing" => $data
];
return response()->json( $result , 200);
}catch(Exception $ex){
return response()->json(["message" => $ex->getMessage(), 500]);
}
}
/**
* Store a newly created resource in storage.
*/

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Http\Controllers\Api;
use App\Enums\ImportDatasourceStatus;
use App\Http\Controllers\Controller;
use App\Models\ImportDatasource;
use App\Traits\GlobalApiResponse;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Http\Request;
class ScrapingController extends Controller
{
use GlobalApiResponse;
/**
* Display a listing of the resource.
*/
public function index()
{
$check_datasource = ImportDatasource::where("status", ImportDatasourceStatus::Processing->value)->count();
if($check_datasource > 0){
return $this->resError("Failed to execute while processing another scraping");
}
// run service artisan command
Artisan::call("app:execute-scraping");
return $this->resSuccess("Success execute scraping service please wait");
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(string $id)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
//
}
}

View File

@@ -2,10 +2,12 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ImportDatasource extends Model
{
use HasFactory;
protected $table = 'import_datasources';
protected $fillable = [
'id',

View File

@@ -26,13 +26,15 @@ class ServiceClient
);
}
public function makeRequest($url, $method = 'GET', $body = null, $headers = []){
public function makeRequest($url, $method = 'GET', $body = null, $headers = [], $timeout = 300){
try {
$headers = array_merge($this->headers, $headers);
$options = [
'headers' => $headers,
'timeout' => $timeout,
'connect_timeout' => 60
];
if ($body) {

View File

@@ -48,21 +48,13 @@ class ServiceSIMBG
return $res;
}
public function syncIndexIntegration($uuid)
public function syncIndexIntegration($uuid, $token)
{
$clientHelper = new ServiceClient($this->simbg_host);
$url = "/api/pbg/v1/detail/" . $uuid . "/retribution/indeks-terintegrasi/";
$resToken = $this->getToken();
if (!isset($resToken) || empty($resToken->original['data']['token']['access'])) {
// Log error
Log::error("Token not retrieved for syncIndexIntegration", ['uuid' => $uuid]);
return null;
}
$apiToken = $resToken->original['data']['token']['access'];
$headers = [
'Authorization' => "Bearer " . $apiToken,
'Authorization' => "Bearer " . $token,
];
$res = $clientHelper->get($url, $headers);
@@ -70,16 +62,16 @@ class ServiceSIMBG
if (empty($res->original['success']) || !$res->original['success']) {
// Log error
Log::error("API response indicates failure", ['url' => $url, 'uuid' => $uuid]);
return null;
return false;
}
$data = $res->original['data']['data'] ?? null;
if (!$data) {
Log::error("No valid data returned from API", ['url' => $url, 'uuid' => $uuid]);
return null;
return false;
}
PbgTaskIndexIntegrations::updateOrCreate(
$resultData = PbgTaskIndexIntegrations::updateOrCreate(
['pbg_task_uid' => $uuid],
[
'indeks_fungsi_bangunan' => $data['indeks_fungsi_bangunan'] ?? null,
@@ -93,19 +85,25 @@ class ServiceSIMBG
);
// Log success
Log::info("syncIndexIntegration completed successfully", ['uuid' => $uuid]);
if ($resultData->wasRecentlyCreated) {
Log::info("integration created successfully", ['uuid' => $uuid]);
} else {
Log::info("integration updated successfully", ['uuid' => $uuid]);
}
return true;
}
public function syncTaskList()
{
$clientHelper = new ServiceClient($this->simbg_host);
$resToken = $this->getToken();
$initResToken = $this->getToken();
$importDatasource = ImportDatasource::create([
'status' => ImportDatasourceStatus::Processing->value,
]);
if (empty($resToken->original['data']['token']['access'])) {
if (empty($initResToken->original['data']['token']['access'])) {
$importDatasource->update([
'status' => ImportDatasourceStatus::Failed->value,
'message' => 'Failed to retrieve token'
@@ -113,14 +111,14 @@ class ServiceSIMBG
return $this->resError("Failed to retrieve token");
}
$apiToken = $resToken->original['data']['token']['access'];
$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 = $clientHelper->get($url, $headers);
$totalPage = $initialResponse->original['data']['total_page'] ?? 0;
if ($totalPage === 0) {
if ($totalPage == 0) {
$importDatasource->update([
'status' => ImportDatasourceStatus::Failed->value,
'message' => 'Invalid response: no total_page'
@@ -132,14 +130,26 @@ class ServiceSIMBG
for ($currentPage = 1; $currentPage <= $totalPage; $currentPage++) {
$pageUrl = "/api/pbg/v1/list/?page={$currentPage}&size={$this->fetch_per_page}&sort=ASC";
$token = $this->getToken();
$getToken = $this->getToken();
if (empty($getToken->original['data']['token']['access'])) {
$importDatasource->update([
'status' => ImportDatasourceStatus::Failed->value,
'message' => 'Failed to retrieve token'
]);
break;
}
$token = $getToken->original['data']['token']['access'];
$headers = ['Authorization' => "Bearer " . $token];
$response = $clientHelper->get($pageUrl, $headers);
$tasks = $response->original['data']['data'] ?? [];
if (empty($tasks)) {
$importDatasource->update([
'status' => ImportDatasourceStatus::Failed->value,
'message' => 'No data found on page'
]);
Log::warning("No data found on page", ['page' => $currentPage]);
continue;
break;
}
Log::info("executed page", ['page' => $currentPage, 'total' => $totalPage]);
@@ -170,15 +180,20 @@ class ServiceSIMBG
'created_at' => now(),
];
$this->syncIndexIntegration($item['uid']);
$this->syncTaskDetailSubmit($item['uid']);
$this->syncIndexIntegration($item['uid'], $token);
$this->syncTaskDetailSubmit($item['uid'], $token);
$savedCount++;
} catch (Exception $e) {
$failedCount++;
$importDatasource->update([
'status' => ImportDatasourceStatus::Failed->value,
'message' => "Successfully processed: $savedCount, Failed: $failedCount"
]);
Log::error("Failed to process task", [
'error' => $e->getMessage(),
'task' => $item,
]);
break;
}
}
@@ -201,21 +216,13 @@ class ServiceSIMBG
}
public function syncTaskDetailSubmit($uuid)
public function syncTaskDetailSubmit($uuid, $token)
{
$clientHelper = new ServiceClient($this->simbg_host);
$resToken = $this->getToken();
if (!isset($resToken) || empty($resToken->original['data']['token']['access'])) {
// Log error
Log::error("Token not retrieved for syncTaskDetailSubmit");
return null;
}
$apiToken = $resToken->original['data']['token']['access'];
$url = "/api/pbg/v1/detail/" . $uuid . "/retribution/submit/";
$headers = [
'Authorization' => "Bearer " . $apiToken,
'Authorization' => "Bearer " . $token,
];
$res = $clientHelper->get($url, $headers);
@@ -223,13 +230,13 @@ class ServiceSIMBG
if (empty($res->original['success']) || !$res->original['success']) {
// Log error
Log::error("API response indicates failure", ['url' => $url, 'uuid' => $uuid]);
return null;
return false;
}
$data = $res->original['data']['data'] ?? [];
if (empty($data)) {
Log::error("No data returned from API", ['url' => $url, 'uuid' => $uuid]);
return null;
return false;
}
$detailCreatedAt = isset($data['created_at'])
@@ -281,7 +288,8 @@ class ServiceSIMBG
PbgTaskPrasarana::upsert($insertData, ['pbg_task_uid', 'prasarana_id']);
}
Log::info("syncTaskDetailSubmit completed successfully", ['uuid' => $uuid]);
Log::info("retribution and prasarana successfully", ['uuid' => $uuid]);
return true;
}
}

View File

@@ -12,10 +12,6 @@ trait GlobalApiResponse
'success' => true,
'data' => $result
];
if(!empty($message)){
$response['message'] = $message;
}
return response()->json($response, $code);
}

View File

@@ -37,7 +37,9 @@ class UsersTable {
url: `${GlobalConfig.apiHost}/api/users`,
credentials: "include",
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
Authorization: `Bearer ${document
.querySelector('meta[name="api-token"]')
.getAttribute("content")}`,
"Content-Type": "application/json",
},
then: (data) =>

View File

@@ -11,15 +11,15 @@ class RequestAssignment {
new Grid({
columns: [
"ID",
"Name",
"Condition",
{name: "Name", width: "15%"},
{name: "Condition", width: "7%"},
"Registration Number",
"Document Number",
"Address",
{name: "Address", width: "30%"},
"Status",
"Function Type",
"Consultation Type",
"Due Date",
{name: "Due Date", width: "7%"},
],
search: {
server: {
@@ -40,7 +40,9 @@ class RequestAssignment {
url: `${GlobalConfig.apiHost}/api/request-assignments`,
credentials: "include",
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
Authorization: `Bearer ${document
.querySelector('meta[name="api-token"]')
.getAttribute("content")}`,
"Content-Type": "application/json",
},
then: (data) =>

View File

@@ -22,8 +22,8 @@ class SyncronizeTask {
console.log("cell data", cell);
return gridjs.html(`
<div class="d-flex justify-items-end gap-10">
<a href="/settings/general/${cell}/edit" class="btn btn-warning me-2">Update</a>
<button class="btn btn-delete btn-delete-global-settings" data-id="${cell}">Delete</button>
<a href="/settings/general/${cell}/edit" class="btn btn-yellow me-2">Update</a>
<button class="btn btn-red btn-delete btn-delete-global-settings" data-id="${cell}">Delete</button>
</div>
`);
},

View File

@@ -6,7 +6,7 @@ import GlobalConfig from "../../global-config.js";
class SyncronizeTask {
init() {
this.initTableImportDatasources();
this.onSyncSubmit();
this.handleSubmitSync();
}
initTableImportDatasources() {
new Grid({
@@ -46,20 +46,71 @@ class SyncronizeTask {
},
}).render(document.getElementById("table-import-datasources"));
}
onSyncSubmit() {
const form = document.getElementById("sync-form");
if (form) {
form.addEventListener("submit", function (event) {
event.preventDefault(); // Prevent the default form submission
const button = document.getElementById("btn-sync-submit");
if (button) {
button.disabled = true;
button.innerText = "Processing...";
}
form.submit();
});
}
handleSubmitSync() {
const button = document.getElementById("btn-sync-submit");
// Check if the button should be enabled or disabled based on the status
fetch(`${GlobalConfig.apiHost}/api/import-datasource/check-datasource`, {
method: "GET",
headers: {
Authorization: `Bearer ${document
.querySelector('meta[name="api-token"]')
.getAttribute("content")}`,
"Content-Type": "application/json",
}
})
.then(response => {
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.json();
})
.then(data => {
console.log("data check button sync", data.can_execute);
button.disabled = !data.can_execute;
// If the button is enabled, add click event to trigger sync
if (!button.disabled) {
button.addEventListener("click", function(e) {
button.disabled = true; // Disable button to prevent multiple clicks
button.textContent = "Syncing..."; // Change button text to show syncing
// Trigger the scraping API call
fetch(`${GlobalConfig.apiHost}/api/scraping`, {
method: "GET",
headers: {
Authorization: `Bearer ${document
.querySelector('meta[name="api-token"]')
.getAttribute("content")}`,
"Content-Type": "application/json",
}
})
.then(response => {
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.json();
})
.then(data => {
console.log("data sync button", data);
alert("Synchronization executed successfully");
window.location.reload();
})
.catch(err => {
console.error("Fetch error:", err);
alert("An error occurred during synchronization");
})
.finally(() => {
button.disabled = false; // Re-enable the button after the request is complete
button.textContent = "Sync Data"; // Reset button text
});
});
}
})
.catch(err => {
console.error("Fetch error:", err);
alert("An error occurred while checking the datasource");
});
}
}
document.addEventListener("DOMContentLoaded", function (e) {

View File

@@ -64,10 +64,10 @@ $blue: #1a80f8;
$indigo: #53389f;
$purple: #7e67fe;
$pink: #ff86c8;
$red: #ed321f;
$red: #bd1300;
$orange: #f0934e;
$yellow: #fb9f68;
$green: #21d760;
$yellow: #dfdc40;
$green: #00c91b;
$teal: #040505;
$cyan: #1ab0f8;
// scss-docs-end color-variables
@@ -81,7 +81,7 @@ $purple: $purple;
$pink: $pink;
$danger: $red;
$orange: $orange;
$warning: $orange;
$warning: $yellow;
$success: $green;
$info: $cyan;
$light: $gray-200;

View File

@@ -11,25 +11,25 @@
</div>
<!-- App Search-->
<form class="app-search d-none d-md-block me-auto">
<!-- <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>
</form> -->
</div>
<div class="d-flex align-items-center gap-2">
<!-- Theme Color (Light/Dark) -->
<div class="topbar-item">
<!-- <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> -->
<iconify-icon icon="solar:sun-2-outline"
class="fs-22 align-middle dark-mode"></iconify-icon>
</button>
</div>
</div> -->
<!-- Notification -->
<div class="dropdown topbar-item">
@@ -156,7 +156,7 @@
<!-- item-->
<h6 class="dropdown-header">Welcome!</h6>
<a class="dropdown-item" href="#">
<!-- <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>
@@ -176,7 +176,7 @@
<iconify-icon icon="solar:lock-keyhole-outline"
class="align-middle me-2 fs-18"></iconify-icon><span class="align-middle">Lock
screen</span>
</a>
</a> -->
<div class="dropdown-divider my-1"></div>

View File

@@ -4,7 +4,7 @@
@include('layouts.partials/page-title', ['title' => 'Se', 'subtitle' => 'Syncronize'])
<div class="row">
<div class="row d-flex justify-content-center">
@if (session('error'))
<div class="alert alert-danger">
{{ session('error') }}
@@ -20,7 +20,7 @@
</ul>
</div>
@endif
<div class="columns-md">
<div class="col-lg-6">
<div class="card">
<div class="card-body">
<form action="{{ route('general.store') }}" method="POST">
@@ -51,5 +51,4 @@
@endsection
@section('scripts')
@vite(['resources/js/tables/common-table.js'])
@endsection

View File

@@ -10,10 +10,11 @@
<div class="row">
<div class="col-md-12 col-xl-12 d-flex justify-content-end">
<form action="{{route('settings.sync')}}" method="post" id="sync-form">
<!-- <form action="{{route('settings.sync')}}" method="post" id="sync-form">
@csrf
<button type="submit" class="btn btn-success width-lg" id="btn-sync-submit">Sync SIMBG</button>
</form>
<button type="button" class="btn btn-success width-lg" id="btn-sync-submit">Sync SIMBG</button>
</form> -->
<button type="button" class="btn btn-success width-lg" id="btn-sync-submit">Sync SIMBG</button>
</div>
<div>
<div id="table-import-datasources"></div>

View File

@@ -4,17 +4,11 @@ use App\Http\Controllers\Api\DashboardController;
use App\Http\Controllers\Api\GlobalSettingsController;
use App\Http\Controllers\Api\ImportDatasourceController;
use App\Http\Controllers\Api\RequestAssignmentController;
use App\Http\Controllers\Api\ScrapingController;
use App\Http\Controllers\Api\UsersController;
use App\Http\Controllers\Settings\SyncronizeController;
use App\Models\PbgTask;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
// Route::get('/user', function (Request $request) {
// return $request->user();
// })->middleware('auth:sanctum');
Route::post('/login', [UsersController::class, 'login'])->name('api.user.login');
Route::group(['middleware' => 'auth:sanctum'], function (){
// users
@@ -25,7 +19,12 @@ Route::group(['middleware' => 'auth:sanctum'], function (){
Route::apiResource('global-settings', GlobalSettingsController::class);
// import datasource
Route::apiResource('import-datasource',ImportDatasourceController::class);
// Route::apiResource('import-datasource',ImportDatasourceController::class);
Route::controller(ImportDatasourceController::class)->group(function (){
Route::get('/import-datasource/check-datasource', 'checkImportDatasource')
->name('import-datasource.check');
Route::get('/import-datasource', 'index')->name('import-datasource.index');
});
// request assignments
Route::apiResource('request-assignments',RequestAssignmentController::class);
@@ -37,6 +36,9 @@ Route::group(['middleware' => 'auth:sanctum'], function (){
Route::get('/all-task-documents', 'allTaskDocuments');
Route::get('/pbg-task-documents', 'pbgTaskDocuments');
});
// scraping
Route::apiResource('/scraping', ScrapingController::class);
});