Compare commits

..

18 Commits

Author SHA1 Message Date
arifal
d95676d477 fix show page document pbg 2025-04-10 15:44:51 +07:00
arifal
7737fee30f add validation topic on send prompt if not relevan 2025-04-10 01:59:57 +07:00
arifal
48293386c7 fix change chatbot to top 2025-04-09 21:22:26 +07:00
arifal
84870b95b1 add feat upload pbg task 2025-04-09 21:10:20 +07:00
arifal
6294d2f950 add new users on seeder for role user and superadmin 2025-03-27 17:19:27 +07:00
arifal
7b70591be5 add readme and backup schema local 2025-03-27 15:28:21 +07:00
arifal
3b67bfd1fb remove duplicate use timeout when running queue 2025-03-26 15:20:03 +07:00
arifal
45e22096ed add column to model import datasources 2025-03-26 11:51:57 +07:00
arifal
c7a8d6d249 add condition show button retry 2025-03-25 22:46:34 +07:00
arifal
091b1f305e fix handle retry button 2025-03-25 15:54:02 +07:00
arifal
e7950e22f2 add flatpicker for edit pbg task data 2025-03-24 21:16:05 +07:00
arifal
b68641db03 backup local db and add global setting seeder to run default 2025-03-24 11:08:10 +07:00
arifal
fefb85ac7a remove get sample data from service 2025-03-21 19:10:17 +07:00
arifal
d28a08a24c fix add bigdata resume after syncronize 2025-03-21 19:08:18 +07:00
arifal
654d2efe19 fix syncronize using worker and add duration syncronize 2025-03-21 18:44:28 +07:00
arifal
0a080763cd try catch handle refresh token fail to login again 2025-03-21 16:03:10 +07:00
arifal
f3ef21d1be fix style error message when failed login 2025-03-21 15:10:28 +07:00
arifal
d7bff86741 fix menu with parent data and remove action google sheet not available feature 2025-03-21 14:54:18 +07:00
49 changed files with 1671 additions and 1078 deletions

View File

@@ -17,10 +17,10 @@ sudo nano /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /home/arifal/development/sibedas-pbg-web/artisan queue:work --queue=default --timeout=40000 --tries=1 --sleep=3
command=php /home/arifal/development/sibedas-pbg-web/artisan queue:work --queue=default --timeout=82800 --tries=1
autostart=true
autorestart=true
numprocs=4
numprocs=1
redirect_stderr=true
stdout_logfile=/home/arifal/development/sibedas-pbg-web/storage/logs/worker.log
stopasgroup=true

View File

@@ -2,10 +2,12 @@
namespace App\Console\Commands;
use App\Jobs\ScrapingDataJob;
use App\Models\ImportDatasource;
use App\Services\ServiceGoogleSheet;
use App\Services\ServicePbgTask;
use App\Services\ServiceTabPbgTask;
use App\Services\ServiceTokenSIMBG;
use GuzzleHttp\Client; // Import Guzzle Client
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
@@ -26,64 +28,68 @@ class ScrapingData extends Command
* @var string
*/
protected $description = 'Command description';
private $client;
private $service_pbg_task;
private $service_tab_pbg_task;
/**
* Inject dependencies.
*/
public function __construct(Client $client, ServicePbgTask $service_pbg_task, ServiceTabPbgTask $serviceTabPbgTask)
{
public function __construct(
) {
parent::__construct();
$this->client = $client;
$this->service_pbg_task = $service_pbg_task;
$this->service_tab_pbg_task = $serviceTabPbgTask;
}
public function handle()
{
dispatch(new ScrapingDataJob());
$this->info("Scraping job dispatched successfully");
}
/**
* Execute the console command.
*/
public function handle()
{
// public function handle()
// {
try {
// Create a record with "processing" status
$import_datasource = ImportDatasource::create([
'message' => 'Initiating scraping...',
'response_body' => null,
'status' => 'processing'
]);
// try {
// // Create a record with "processing" status
// $import_datasource = ImportDatasource::create([
// 'message' => 'Initiating scraping...',
// 'response_body' => null,
// 'status' => 'processing',
// 'start_time' => now()
// ]);
// Run the service
$service_google_sheet = new ServiceGoogleSheet();
$service_google_sheet->run_service();
// // Run the service
// $service_google_sheet = new ServiceGoogleSheet();
// $service_google_sheet->run_service();
// Run the ServicePbgTask with injected Guzzle Client
$this->service_pbg_task->run_service();
// // Run the ServicePbgTask with injected Guzzle Client
// $this->service_pbg_task->run_service();
// run the service pbg task assignments
$this->service_tab_pbg_task->run_service();
// // run the service pbg task assignments
// $this->service_tab_pbg_task->run_service();
// Update the record status to "success" after completion
$import_datasource->update([
'status' => 'success',
'message' => 'Scraping completed successfully.'
]);
// // Update the record status to "success" after completion
// $import_datasource->update([
// 'status' => 'success',
// 'message' => 'Scraping completed successfully.',
// 'finish_time' => now()
// ]);
} catch (\Exception $e) {
// } catch (\Exception $e) {
// // Log the error for debugging
// Log::error('Scraping failed: ' . $e->getMessage(), ['trace' => $e->getTraceAsString()]);
// // Handle errors by updating the status to "failed"
// if (isset($import_datasource)) {
// $import_datasource->update([
// 'status' => 'failed',
// 'response_body' => 'Error: ' . $e->getMessage(),
// 'finish_time' => now()
// ]);
// }
// }
// }
// Log the error for debugging
Log::error('Scraping failed: ' . $e->getMessage(), ['trace' => $e->getTraceAsString()]);
// Handle errors by updating the status to "failed"
if (isset($import_datasource)) {
$import_datasource->update([
'status' => 'failed',
'response_body' => 'Error: ' . $e->getMessage()
]);
}
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Enums;
enum PbgTaskApplicationTypes: string
{
case PERSETUJUAN_BG = '1';
case PERUBAHAN_BG = '2';
case SLF_BB = '4';
case SLF = '5';
public static function labels(): array
{
return [
null => "Pilih Application Type",
self::PERSETUJUAN_BG->value => 'Persetujuan Bangunan Gedung',
self::PERUBAHAN_BG->value => 'Perubahan Bangunan Gedung',
self::SLF_BB->value => 'Sertifikat Laik Fungsi - Bangunan Baru',
self::SLF->value => 'Sertifikat Laik Fungsi',
];
}
public static function getLabel(?string $status): ?string
{
return self::labels()[$status] ?? null;
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Enums;
enum PbgTaskStatus: int
{
case VERIFIKASI_KELENGKAPAN = 1;
case PERBAIKAN_DOKUMEN = 2;
case PERMOHONAN_DIBATALKAN = 3;
case MENUNGGU_PENUGASAN_TPT_TPA = 4;
case MENUNGGU_JADWAL_KONSULTASI = 5;
case PELAKSANAAN_KONSULTASI = 6;
case PERBAIKAN_DOKUMEN_KONSULTASI = 8;
case PERMOHONAN_DITOLAK = 9;
case PERHITUNGAN_RETRIBUSI = 10;
case MENUNGGU_PEMBAYARAN_RETRIBUSI = 14;
case VERIFIKASI_PEMBAYARAN_RETRIBUSI = 15;
case RETRIBUSI_TIDAK_SESUAI = 16;
case VERIFIKASI_SK_PBG = 18;
case PENERBITAN_SK_PBG = 19;
case SK_PBG_TERBIT = 20;
case PENERBITAN_SPPST = 24;
case PROSES_PENERBITAN_SKRD = 25;
case MENUNGGU_PENUGASAN_TPT = 26;
case VERIFIKASI_DATA_TPT = 27;
case SERTIFIKAT_SLF_TERBIT = 28;
public static function getStatuses(): array
{
return [
null => "Pilih Status",
self::VERIFIKASI_KELENGKAPAN->value => "Verifikasi Kelengkapan Dokumen",
self::PERBAIKAN_DOKUMEN->value => "Perbaikan Dokumen",
self::PERMOHONAN_DIBATALKAN->value => "Permohonan Dibatalkan",
self::MENUNGGU_PENUGASAN_TPT_TPA->value => "Menunggu Penugasan TPT/TPA",
self::MENUNGGU_JADWAL_KONSULTASI->value => "Menunggu Jadwal Konsultasi",
self::PELAKSANAAN_KONSULTASI->value => "Pelaksanaan Konsultasi",
self::PERBAIKAN_DOKUMEN_KONSULTASI->value => "Perbaikan Dokumen Konsultasi",
self::PERMOHONAN_DITOLAK->value => "Permohonan Ditolak",
self::PERHITUNGAN_RETRIBUSI->value => "Perhitungan Retribusi",
self::MENUNGGU_PEMBAYARAN_RETRIBUSI->value => "Menunggu Pembayaran Retribusi",
self::VERIFIKASI_PEMBAYARAN_RETRIBUSI->value => "Verifikasi Pembayaran Retribusi",
self::RETRIBUSI_TIDAK_SESUAI->value => "Retribusi Tidak Sesuai",
self::VERIFIKASI_SK_PBG->value => "Verifikasi SK PBG",
self::PENERBITAN_SK_PBG->value => "Penerbitan SK PBG",
self::SK_PBG_TERBIT->value => "SK PBG Terbit",
self::PENERBITAN_SPPST->value => "Penerbitan SPPST",
self::PROSES_PENERBITAN_SKRD->value => "Proses Penerbitan SKRD",
self::MENUNGGU_PENUGASAN_TPT->value => "Menunggu Penugasan TPT",
self::VERIFIKASI_DATA_TPT->value => "Verifikasi Data TPT",
self::SERTIFIKAT_SLF_TERBIT->value => "Sertifikat SLF Terbit",
];
}
public static function getLabel(?int $status): ?string
{
return self::getStatuses()[$status] ?? null;
}
}

View File

@@ -34,7 +34,7 @@ class ChatbotController extends Controller
};
$chatHistory = $request->input('chatHistory');
// Log::info('Chat history sebelum disimpan:', ['history' => $chatHistory]);
Log::info('Chat history sebelum disimpan:', ['history' => $chatHistory]);
if ($main_content === "UNKNOWN") {
return response()->json(['response' => 'Invalid tab_active value.'], 400);
@@ -43,15 +43,16 @@ class ChatbotController extends Controller
// info($main_content);
$queryResponse = $this->openAIService->generateQueryBasedMainContent($request->input('prompt'), $main_content, $chatHistory);
$firstValidation = $this->openAIService->validateSyntaxQuery($queryResponse);
$secondValidation = $this->openAIService->validateSyntaxQuery($queryResponse);
if (str_contains($queryResponse, 'tidak relevan') || str_contains($queryResponse, 'tidak valid') || str_starts_with($queryResponse, 'Prompt')) {
return response()->json(['response' => $queryResponse], 400);
}
$formattedResultQuery = "[]";
$queryResponse = str_replace(['```sql', '```'], '', $queryResponse);
$resultQuery = DB::select($queryResponse);
$formattedResultQuery = json_encode($resultQuery, JSON_PRETTY_PRINT);
// info($formattedResultQuery);
info($formattedResultQuery);
$nlpResult = $this->openAIService->generateNLPFromQuery($request->input('prompt'), $formattedResultQuery);
$finalGeneratedText =$this->openAIService->generateFinalText($nlpResult);
@@ -92,9 +93,6 @@ class ChatbotController extends Controller
$queryResponse = $this->openAIService->createMainQuery($classifyResponse, $request->input('prompt'), $chatHistory);
info($queryResponse);
$firstValidation = $this->openAIService->validateSyntaxQuery($queryResponse);
$secondValidation = $this->openAIService->validateSyntaxQuery($queryResponse);
$formattedResultQuery = "[]";
$queryResponse = str_replace(['```sql', '```'], '', $queryResponse);

View File

@@ -22,7 +22,8 @@ class MenusController extends Controller
$query = $query->where("name", "like", "%".$request->get("search")."%");
}
return response()->json($query->paginate(config('app.paginate_per_page', 50)));
// return response()->json($query->paginate(config('app.paginate_per_page', 50)));
return MenuResource::collection($query->paginate(config('app.paginate_per_page',50)));
}
/**

View File

@@ -0,0 +1,109 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\PbgTaskAttachment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\Response;
class PbgTaskAttachmentsController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request, $pbg_task_id)
{
try{
$request->validate([
'file' => 'required|file|mimes:jpg,png,pdf|max:5120',
'pbg_type' => 'string'
]);
$attachment = PbgTaskAttachment::create([
'pbg_task_id' => $pbg_task_id,
'file_name' => $request->file('file')->getClientOriginalName(),
'file_path' => '', // empty path initially
'pbg_type' => $request->pbg_type == 'bukti_bayar' ? 'bukti_bayar' : 'berita_acara'
]);
$file = $request->file('file');
$path = $file->store("uploads/pbg-tasks/{$pbg_task_id}/{$attachment->id}", "public");
$attachment->update([
'file_path' => $path,
]);
return response()->json([
'message' => 'File uploaded successfully.',
'attachment' => [
'id' => $attachment->id,
'file_name' => $attachment->file_name,
'file_url' => Storage::url($attachment->file_path),
'pbg_type' => $attachment->pbg_type
]
]);
}catch(\Exception $e){
\Log::error($e->getMessage());
return response()->json([
"success" => false,
"message" => $e->getTraceAsString()
]);
}
}
/**
* 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)
{
//
}
public function download(string $id)
{
try {
$data = PbgTaskAttachment::findOrFail($id);
$filePath = $data->file_path; // already relative to 'public' disk
if (!Storage::disk('public')->exists($filePath)) {
return response()->json([
"success" => false,
"message" => "File not found on server"
], Response::HTTP_NOT_FOUND);
}
return Storage::disk('public')->download($filePath, $data->file_name);
} catch (\Exception $e) {
return response()->json([
"success" => false,
"message" => $e->getMessage()
]);
}
}
}

View File

@@ -3,6 +3,8 @@
namespace App\Http\Controllers\Api;
use App\Enums\ImportDatasourceStatus;
use App\Enums\PbgTaskApplicationTypes;
use App\Enums\PbgTaskStatus;
use App\Http\Controllers\Controller;
use App\Http\Requests\PbgTaskMultiStepRequest;
use App\Http\Resources\PbgTaskResource;
@@ -14,6 +16,7 @@ use App\Services\GoogleSheetService;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rules\Enum;
class PbgTaskController extends Controller
{
@@ -116,9 +119,63 @@ class PbgTaskController extends Controller
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
public function update(Request $request, string $task_uuid)
{
//
try{
$pbg_task = PbgTask::where('uuid',$task_uuid)->first();
if(!$pbg_task){
return response()->json([
"success"=> false,
"message"=> "Data PBG Task tidak ditemukan",
], 404);
}
$validated = $request->validate([
'name' => 'required|string|max:255',
'owner_name' => 'required|string|max:255',
'application_type' => ['nullable', new Enum(PbgTaskApplicationTypes::class)],
'condition' => 'required|string|max:255',
'registration_number' => 'required|string|max:255',
'document_number' => 'required|string|max:255',
'status' => ['nullable', new Enum(PbgTaskStatus::class)],
'address' => 'required|string|max:255',
'slf_status_name' => 'nullable|string|max:255',
'function_type' => 'required|string|max:255',
'consultation_type' => 'required|string|max:255',
'due_date' => 'nullable|date|after_or_equal:today',
]);
$statusLabel = $validated['status'] !== null ? PbgTaskStatus::getLabel($validated['status']) : null;
$applicationLabel = $validated['application_type'] !== null ? PbgTaskApplicationTypes::getLabel($validated['application_type']) : null;
$pbg_task->update([
'name' => $validated['name'],
'owner_name' => $validated['owner_name'],
'application_type' => $validated['application_type'],
'application_type_name' => $applicationLabel, // Automatically set application_type_name
'condition' => $validated['condition'],
'registration_number' => $validated['registration_number'],
'document_number' => $validated['document_number'],
'status' => $validated['status'],
'status_name' => $statusLabel, // Automatically set status_name
'address' => $validated['address'],
'slf_status_name' => $validated['slf_status_name'],
'function_type' => $validated['function_type'],
'consultation_type' => $validated['consultation_type'],
'due_date' => $validated['due_date'],
]);
return response()->json([
"success"=> true,
"message"=> "Data berhasil diubah",
"data"=> $pbg_task
]);
}catch(\Exception $e){
return response()->json([
"success"=> false,
"message"=> $e->getMessage(),
]);
}
}
/**
@@ -135,255 +192,4 @@ class PbgTaskController extends Controller
]);
}
public function syncPbgFromGoogleSheet(){
$import_datasource = ImportDatasource::create([
"message" => "initialization",
"response_body" => null,
"status" => ImportDatasourceStatus::Processing->value,
]);
try{
$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"] = $row[2] ?? null;
$data_setting_result["MENUNGGU_KLIK_DPMPTSP_SUM"] = $row[3] ?? null;
} elseif ($found_section === "REALISASI_TERBIT_PBG") {
$data_setting_result["REALISASI_TERBIT_PBG_COUNT"] = $row[2] ?? null;
$data_setting_result["REALISASI_TERBIT_PBG_SUM"] = $row[4] ?? null;
} elseif ($found_section === "PROSES_DINAS_TEKNIS") {
$data_setting_result["PROSES_DINAS_TEKNIS_COUNT"] = $row[2] ?? null;
$data_setting_result["PROSES_DINAS_TEKNIS_SUM"] = $row[3] ?? null;
}
// Reset section tracking after capturing "Grand Total"
$found_section = null;
}
}
foreach ($data_setting_result as $key => $value) {
DataSetting::updateOrInsert(
["key" => $key], // Find by key
["value" => $value] // Update or insert value
);
}
$mapToUpsert = [];
$count = 0;
foreach($sheetData as $data){
$mapToUpsert[] =
[
'no_registrasi' => $data['no__registrasi'] ?? null,
'jenis_konsultasi' => $data['jenis_konsultasi'] ?? null,
'fungsi_bg' => $data['fungsi_bg'] ?? null,
'tgl_permohonan' => $this->convertToDate($data['tgl_permohonan']),
'status_verifikasi' => $data['status_verifikasi'] ?? null,
'status_permohonan' => $this->convertToDate($data['status_permohonan']),
'alamat_pemilik' => $data['alamat_pemilik'] ?? null,
'no_hp' => $data['no__hp'] ?? null,
'email' => $data['e_mail'] ?? null,
'tanggal_catatan' => $this->convertToDate($data['tanggal_catatan']),
'catatan_kekurangan_dokumen' => $data['catatan_kekurangan_dokumen'] ?? null,
'gambar' => $data['gambar'] ?? null,
'krk_kkpr' => $data['krk_kkpr'] ?? null,
'no_krk' => $data['no__krk'] ?? null,
'lh' => $data['lh'] ?? null,
'ska' => $data['ska'] ?? null,
'keterangan' => $data['keterangan'] ?? null,
'helpdesk' => $data['helpdesk'] ?? null,
'pj' => $data['pj'] ?? null,
'kepemilikan' => $data['kepemilikan'] ?? null,
'potensi_taru' => $data['potensi_taru'] ?? null,
'validasi_dinas' => $data['validasi_dinas'] ?? null,
'kategori_retribusi' => $data['kategori_retribusi'] ?? null,
'no_urut_ba_tpt' => $data['no__urut_ba_tpt__2024_0001_'] ?? null,
'tanggal_ba_tpt' => $this->convertToDate($data['tanggal_ba_tpt']),
'no_urut_ba_tpa' => $data['no__urut_ba_tpa'] ?? null,
'tanggal_ba_tpa' => $this->convertToDate($data['tanggal_ba_tpa']),
'no_urut_skrd' => $data['no__urut_skrd__2024_0001_'] ?? null,
'tanggal_skrd' => $this->convertToDate($data['tanggal_skrd']),
'ptsp' => $data['ptsp'] ?? null,
'selesai_terbit' => $data['selesai_terbit'] ?? null,
'tanggal_pembayaran' => $this->convertToDate($data['tanggal_pembayaran__yyyy_mm_dd_']),
'format_sts' => $data['format_sts'] ?? null,
'tahun_terbit' => (int) $data['tahun_terbit'] ?? null,
'tahun_berjalan' => (int) $data['tahun_berjalan'] ?? null,
'kelurahan' => $data['kelurahan'] ?? null,
'kecamatan' => $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' => $data['latitude'] ?? null,
'longitude' => $data['longitude'] ?? null,
'nik_nib' => $data['nik_nib'] ?? null,
'dok_tanah' => $data['dok__tanah'] ?? null,
'temuan' => $data['temuan'] ?? null,
];
}
DB::beginTransaction();
$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',
]);
}
$total_data = count($mapToUpsert);
$import_datasource->update([
"message" => "Successfully processed: {$total_data}",
"status" => ImportDatasourceStatus::Success->value,
]);
DB::commit();
return response()->json([
"success" => true,
"message" => "Data berhasil disimpan ke database"
], 200);
}catch(\Exception $ex){
DB::rollBack();
$import_datasource->update([
"message" => "Failed to importing",
"response_body" => $ex->getMessage(),
"status" => ImportDatasourceStatus::Failed->value,
]);
return response()->json([
"success" => false,
"message" => "Gagal menyimpan data",
"error" => $ex->getMessage()
], 500);
}
}
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;
}
// Otherwise, cast to integer
return (int) $value;
}
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;
}
}
}

View File

@@ -21,12 +21,18 @@ class RequestAssignmentController extends Controller
*/
public function index(Request $request)
{
$query = PbgTask::query()->orderBy('id', 'desc');
if($request->has('search') && !empty($request->get("search"))){
$query->where('name', 'LIKE', '%'.$request->get('search').'%')
->orWhere('registration_number', 'LIKE', '%'.$request->get('search').'%')
->orWhere('document_number', 'LIKE', '%'.$request->get('search').'%');
$query = PbgTask::with(['attachments' => function ($q) {
$q->whereIn('pbg_type', ['berita_acara', 'bukti_bayar']);
}])->orderBy('id', 'desc');
if ($request->has('search') && !empty($request->get("search"))) {
$query->where(function ($q) use ($request) {
$q->where('name', 'LIKE', '%' . $request->get('search') . '%')
->orWhere('registration_number', 'LIKE', '%' . $request->get('search') . '%')
->orWhere('document_number', 'LIKE', '%' . $request->get('search') . '%');
});
}
return RequestAssignmentResouce::collection($query->paginate());
}

View File

@@ -4,9 +4,16 @@ namespace App\Http\Controllers\Api;
use App\Enums\ImportDatasourceStatus;
use App\Http\Controllers\Controller;
use App\Jobs\RetrySyncronizeJob;
use App\Jobs\ScrapingDataJob;
use App\Jobs\SyncronizeSIMBG;
use App\Models\ImportDatasource;
use App\Traits\GlobalApiResponse;
use App\Services\ServiceTokenSIMBG;
use GuzzleHttp\Client;
use App\Services\ServiceGoogleSheet;
use App\Services\ServicePbgTask;
use App\Services\ServiceTabPbgTask;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Http\Request;
@@ -23,40 +30,33 @@ class ScrapingController extends Controller
return $this->resError("Failed to execute while processing another scraping");
}
// use ole schema synchronization
// dispatch(new SyncronizeSIMBG());
Artisan::call("app:scraping-data");
// use new schema synchronization
dispatch(new ScrapingDataJob());
return $this->resSuccess(["message" => "Success execute scraping service on background, check status for more"]);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
public function retry_syncjob(string $import_datasource_id){
try{
/**
* Display the specified resource.
*/
public function show(string $id)
{
//
}
$import_datasource = ImportDatasource::find($import_datasource_id);
if(!$import_datasource){
return $this->resError("Invalid import datasource id", null, 404);
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
//
dispatch(new RetrySyncronizeJob($import_datasource->id));
return response()->json([
"success" => true,
"message" => "Retrying scrape job on background, check status for more"
]);
}catch(\Exception $e){
return response()->json([
"success" => false,
"message" => "Failed to retry sync job",
"error" => $e->getMessage()
]);
}
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers;
use App\Models\PbgTask;
use App\Models\PbgTaskAttachment;
use Illuminate\Http\Request;
class PbgTaskAttachmentsController extends Controller
{
public function show(string $id, Request $request){
try{
$title = $request->get('type') == "berita-acara" ? "Berita Acara" : "Bukti Bayar";
$data = PbgTaskAttachment::findOrFail($id);
$pbg = PbgTask::findOrFail($data->pbg_task_id);
return view('pbg-task-attachment.show', compact('data', 'pbg', 'title'));
}catch(\Exception $e){
return view('pages.404');
}
}
}

View File

@@ -2,11 +2,13 @@
namespace App\Http\Controllers\RequestAssignment;
use App\Enums\PbgTaskApplicationTypes;
use App\Http\Controllers\Controller;
use App\Models\PbgTask;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
use App\Enums\PbgTaskStatus;
class PbgTaskController extends Controller
{
@@ -60,7 +62,9 @@ class PbgTaskController extends Controller
public function show(string $id)
{
$data = PbgTask::with(['pbg_task_retributions','pbg_task_index_integrations', 'pbg_task_retributions.pbg_task_prasarana'])->findOrFail($id);
return view("pbg_task.show", compact("data"));
$statusOptions = PbgTaskStatus::getStatuses();
$applicationTypes = PbgTaskApplicationTypes::labels();
return view("pbg_task.show", compact("data", 'statusOptions', 'applicationTypes'));
}
/**

View File

@@ -2,6 +2,7 @@
namespace App\Http\Resources;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
@@ -14,13 +15,21 @@ class ImportDatasourceResource extends JsonResource
*/
public function toArray(Request $request): array
{
$startTime = $this->start_time ? Carbon::parse($this->start_time) : null;
$finishTime = $this->finish_time ? Carbon::parse($this->finish_time) : null;
return [
"id"=> $this->id,
"message" => $this->message,
"response_body" => $this->response_body,
"status" => $this->status,
"start_time" => $startTime ? $startTime->toDateTimeString() : null,
"duration" => ($startTime && $finishTime)
? $finishTime->diff($startTime)->format('%H:%I:%S')
: null,
"finish_time" => $finishTime ? $finishTime->toDateTimeString() : null,
"created_at" => $this->created_at->toDateTimeString(),
"updated_at" => $this->updated_at->toDateTimeString(),
"failed_uuid" => $this->failed_uuid
];
}
}

View File

@@ -14,6 +14,16 @@ class MenuResource extends JsonResource
*/
public function toArray(Request $request): array
{
return parent::toArray($request);
// return parent::toArray($request);
return [
'id' => $this->id,
'name' => $this->name,
'icon' => $this->icon,
'url' => $this->url,
'sort_order' => $this->sort_order,
'parent' => $this->parent ? new MenuResource($this->parent) : null,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at
];
}
}

View File

@@ -34,6 +34,14 @@ class RequestAssignmentResouce extends JsonResource
'due_date' => $this->due_date,
'land_certificate_phase' => $this->land_certificate_phase,
'task_created_at' => $this->task_created_at,
'attachment_berita_acara' => $this->attachments
->where('pbg_type', 'berita_acara')
->sortByDesc('created_at')
->first(),
'attachment_bukti_bayar' => $this->attachments
->where('pbg_type', 'bukti_bayar')
->sortByDesc('created_at')
->first(),
];
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Jobs;
use App\Enums\ImportDatasourceStatus;
use App\Models\BigdataResume;
use App\Models\ImportDatasource;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use App\Services\ServiceTabPbgTask;
use App\Services\ServiceGoogleSheet;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class RetrySyncronizeJob implements ShouldQueue
{
use Queueable, Dispatchable, InteractsWithQueue, SerializesModels;
private $import_datasource_id;
public function __construct(int $import_datasource_id)
{
$this->import_datasource_id = $import_datasource_id;
}
/**
* Execute the job.
*/
public function handle(): void
{
try{
$service_tab_pbg_task = app(ServiceTabPbgTask::class);
$service_google_sheet = app(ServiceGoogleSheet::class);
$failed_import = ImportDatasource::find($this->import_datasource_id);
$failed_import->update([
'message' => "Retrying from UUID: ". $failed_import->failed_uuid,
'status' => ImportDatasourceStatus::Processing->value,
'start_time' => now()
]);
$current_failed_uuid = null;
try{
$service_tab_pbg_task->run_service($failed_import->failed_uuid);
}catch(\Exception $e){
$current_failed_uuid = $service_tab_pbg_task->getFailedUUID();
throw $e;
}
$data_setting_result = $service_google_sheet->get_big_resume_data();
BigdataResume::generateResumeData($failed_import->id, "all", $data_setting_result);
BigdataResume::generateResumeData($failed_import->id, now()->year, $data_setting_result);
$failed_import->update([
'status' => ImportDatasourceStatus::Success->value,
'message' => "Retry completed successfully from UUID: ". $failed_import->failed_uuid,
'finish_time' => now(),
'failed_uuid' => null
]);
}catch(\Exception $e){
\Log::error("RetrySyncronizeJob Failed: ". $e->getMessage(), [
'exception' => $e,
]);
if(isset($failed_import)){
$failed_import->update([
'status' => ImportDatasourceStatus::Failed->value,
'message' => "Retry failed from UUID: ". $failed_import->failed_uuid,
'finish_time' => now(),
'failed_uuid' => $current_failed_uuid
]);
}
$this->fail($e);
}
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace App\Jobs;
use App\Models\BigdataResume;
use App\Models\ImportDatasource;
use App\Services\ServiceGoogleSheet;
use App\Services\ServicePbgTask;
use App\Services\ServiceTabPbgTask;
use App\Services\ServiceTokenSIMBG;
use GuzzleHttp\Client;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class ScrapingDataJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Inject dependencies instead of creating them inside.
*/
public function __construct(
) {
}
/**
* Execute the job.
*/
public function handle()
{
try {
$client = app(Client::class);
$service_pbg_task = app(ServicePbgTask::class);
$service_tab_pbg_task = app(ServiceTabPbgTask::class);
$service_google_sheet = app(ServiceGoogleSheet::class);
$service_token = app(ServiceTokenSIMBG::class);
// Create a record with "processing" status
$import_datasource = ImportDatasource::create([
'message' => 'Initiating scraping...',
'response_body' => null,
'status' => 'processing',
'start_time' => now(),
'failed_uuid' => null
]);
$failed_uuid = null;
// Run the scraping services
$service_google_sheet->run_service();
$service_pbg_task->run_service();
try{
$service_tab_pbg_task->run_service();
}catch(\Exception $e){
$failed_uuid = $service_tab_pbg_task->getFailedUUID();
throw $e;
}
$data_setting_result = $service_google_sheet->get_big_resume_data();
BigdataResume::generateResumeData($import_datasource->id, "all", $data_setting_result);
BigdataResume::generateResumeData($import_datasource->id, now()->year, $data_setting_result);
// Update status to success
$import_datasource->update([
'status' => 'success',
'message' => 'Scraping completed successfully.',
'finish_time' => now()
]);
} catch (\Exception $e) {
Log::error('Scraping failed: ' . $e->getMessage(), ['trace' => $e->getTraceAsString()]);
// Update status to failed
if (isset($import_datasource)) {
$import_datasource->update([
'status' => 'failed',
'response_body' => 'Error: ' . $e->getMessage(),
'finish_time' => now(),
'failed_uuid' => $failed_uuid,
]);
}
// Mark the job as failed
$this->fail($e);
}
}
}

View File

@@ -13,6 +13,9 @@ class ImportDatasource extends Model
'id',
'message',
'response_body',
'status'
'status',
'start_time',
'finish_time',
'failed_uuid'
];
}

View File

@@ -22,4 +22,7 @@ class Menu extends Model
public function children(){
return $this->hasMany(Menu::class,'parent_id');
}
public function parent(){
return $this->belongsTo(Menu::class,'parent_id');
}
}

View File

@@ -46,4 +46,8 @@ class PbgTask extends Model
{
return $this->hasMany(TaskAssignment::class, 'pbg_task_uid', 'uuid');
}
public function attachments(){
return $this->hasMany(PbgTaskAttachment::class, 'pbg_task_id', 'id');
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PbgTaskAttachment extends Model
{
protected $fillable = ['pbg_task_id', 'file_name', 'file_path', 'pbg_type'];
public function task(){
return $this->belongsTo(PbgTask::class);
}
}

View File

@@ -3,8 +3,13 @@
namespace App\Providers;
use App\Models\Menu;
use App\Services\ServiceGoogleSheet;
use App\Services\ServicePbgTask;
use App\Services\ServiceTabPbgTask;
use App\Services\ServiceTokenSIMBG;
use App\View\Components\Circle;
use Auth;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
@@ -19,11 +24,24 @@ class AppServiceProvider extends ServiceProvider
*/
public function register(): void
{
$this->app->singleton(GoogleSheetService::class, function () {
return new GoogleSheetService();
$this->app->bind(Client::class, function () {
return new Client();
});
$this->app->singleton(ServiceSIMBG::class, function ($app) {
return new ServiceSIMBG($app->make(GoogleSheetService::class));
$this->app->bind(ServiceTokenSIMBG::class, function ($app) {
return new ServiceTokenSIMBG();
});
$this->app->bind(ServicePbgTask::class, function ($app) {
return new ServicePbgTask($app->make(Client::class), $app->make(ServiceTokenSIMBG::class));
});
$this->app->bind(ServiceTabPbgTask::class, function ($app) {
return new ServiceTabPbgTask($app->make(Client::class), $app->make(ServiceTokenSIMBG::class));
});
$this->app->bind(ServiceGoogleSheet::class, function ($app) {
return new ServiceGoogleSheet();
});
}

View File

@@ -27,6 +27,11 @@ class OpenAIService
return "Template prompt tidak ditemukan.";
}
$validationResponse = $this->validatePromptTopic($prompt);
if ($validationResponse !== 'VALID') {
return "Prompt yang Anda masukkan tidak relevan dengan data PUPR/SIMBG/DPUTR atau pekerjaan sejenis.";
}
// Ambil template berdasarkan kategori
$promptTemplate = $jsonData[$mainContent]['prompt'];
@@ -273,4 +278,26 @@ class OpenAIService
// return trim($response['choices'][0]['message']['content'] ?? 'No response');
// }
public function validatePromptTopic($prompt)
{
$messages = [
[
'role' => 'system',
'content' => "You are a classification expert. Determine if the user's request is related to the Indonesian Ministry of Public Works and Public Housing (PUPR), DPUTR, SIMBG, or construction-related tasks managed by these institutions.
Only respond with:
- VALID if it's relevant to those topics
- INVALID if not related at all"
],
['role' => 'user', 'content' => $prompt],
];
$response = $this->client->chat()->create([
'model' => 'gpt-4o-mini',
'messages' => $messages,
]);
return trim($response['choices'][0]['message']['content'] ?? 'INVALID');
}
}

View File

@@ -44,7 +44,7 @@ class ServiceGoogleSheet
if (empty($sheet_data) || count($sheet_data) < 2) {
Log::warning("sync_google_sheet_data: No valid data found.");
throw new \Exception("sync_google_sheet_data: No valid data found.");
throw new Exception("sync_google_sheet_data: No valid data found.");
}
$cleanValue = function ($value) {
@@ -210,6 +210,47 @@ class ServiceGoogleSheet
}
}
public function get_big_resume_data(){
try {
$sheet_big_data = $this->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;
}
}
return $data_setting_result;
}catch(Exception $exception){
Log::error("Error getting big resume data", ['error' => $exception->getMessage()]);
throw $exception;
}
}
private function get_data_by_sheet($no_sheet = 1){
$spreadsheet = $this->service->spreadsheets->get($this->spreadsheetID);
$sheets = $spreadsheet->getSheets();

View File

@@ -158,8 +158,6 @@ class ServicePbgTask
]);
}
Log::info("Page {$currentPage} fetched & saved", ['records' => count($saved_data)]);
$currentPage++;
} while ($currentPage <= $totalPage);

View File

@@ -19,6 +19,7 @@ class ServiceTabPbgTask
private $service_token;
private $user_token;
private $user_refresh_token;
protected $current_uuid = null;
public function __construct(Client $client, ServiceTokenSIMBG $service_token)
{
@@ -34,25 +35,42 @@ class ServiceTabPbgTask
$this->user_refresh_token = $auth_data['refresh'];
}
public function run_service()
public function run_service($retry_uuid = null)
{
try {
$pbg_tasks = PbgTask::all();
$pbg_tasks = PbgTask::orderBy('id')->get();
$start = false;
foreach ($pbg_tasks as $pbg_task) {
$this->scraping_task_assignments($pbg_task->uuid);
$this->scraping_task_retributions($pbg_task->uuid);
$this->scraping_task_integrations($pbg_task->uuid);
if($retry_uuid){
if($pbg_task->uuid === $retry_uuid){
$start = true;
}
// Process task assignments here if needed
Log::info("Successfully fetched for UUID: {$pbg_task->uuid}");
if(!$start){
continue;
}
}
try{
$this->current_uuid = $pbg_task->uuid;
$this->scraping_task_assignments($pbg_task->uuid);
$this->scraping_task_retributions($pbg_task->uuid);
$this->scraping_task_integrations($pbg_task->uuid);
}catch(\Exception $e){
Log::error("Failed on UUID: {$this->current_uuid}, Error: " . $e->getMessage());
throw $e;
}
}
} catch (\Exception $e) {
Log::error("Failed to scrape task assignments: " . $e->getMessage());
Log::error("Failed to syncronize: " . $e->getMessage());
throw $e;
}
}
public function getFailedUUID(){
return $this->current_uuid;
}
private function scraping_task_assignments($uuid)
{
$url = "{$this->simbg_host}/api/pbg/v1/list-tim-penilai/{$uuid}/?page=1&size=10";
@@ -115,10 +133,15 @@ class ServiceTabPbgTask
} catch (\GuzzleHttp\Exception\ClientException $e) {
if ($e->getCode() === 401 && !$retriedAfter401) {
Log::warning("401 Unauthorized - Refreshing token and retrying...");
$this->refreshToken();
$options['headers']['Authorization'] = "Bearer {$this->user_token}";
$retriedAfter401 = true;
continue; // Retry with new token
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;
}
}
throw $e;
@@ -221,10 +244,15 @@ class ServiceTabPbgTask
} catch (\GuzzleHttp\Exception\ClientException $e) {
if ($e->getCode() === 401 && !$retriedAfter401) {
Log::warning("401 Unauthorized - Refreshing token and retrying...");
$this->refreshToken();
$options['headers']['Authorization'] = "Bearer {$this->user_token}";
$retriedAfter401 = true;
continue;
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;
@@ -295,10 +323,15 @@ class ServiceTabPbgTask
} catch (\GuzzleHttp\Exception\ClientException $e) {
if ($e->getCode() === 401 && !$retriedAfter401) {
Log::warning("401 Unauthorized - Refreshing token and retrying...");
$this->refreshToken();
$options['headers']['Authorization'] = "Bearer {$this->user_token}";
$retriedAfter401 = true;
continue;
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;
@@ -329,22 +362,64 @@ class ServiceTabPbgTask
private function refreshToken()
{
try {
$maxRetries = 3; // Maximum retry attempts
$attempt = 0;
$newAuthToken = $this->service_token->refresh_token($this->user_refresh_token);
while ($attempt < $maxRetries) {
try {
$attempt++;
Log::info("Attempt $attempt: Refreshing token...");
$this->user_token = $newAuthToken['access'];
$this->user_refresh_token = $newAuthToken['refresh'];
$newAuthToken = $this->service_token->refresh_token($this->user_refresh_token);
if (!$this->user_token) {
Log::error("Token refresh failed: No token received.");
throw new \Exception("Failed to refresh token.");
if (!isset($newAuthToken['access']) || !isset($newAuthToken['refresh'])) {
throw new \Exception("Invalid refresh token response.");
}
$this->user_token = $newAuthToken['access'];
$this->user_refresh_token = $newAuthToken['refresh'];
Log::info("Token refreshed successfully on attempt $attempt.");
return; // Exit function on success
} catch (\Exception $e) {
Log::error("Token refresh failed on attempt $attempt: " . $e->getMessage());
if ($attempt >= $maxRetries) {
Log::info("Max retries reached. Attempting to log in again...");
break;
}
sleep(30); // Wait for 30 seconds before retrying
}
}
Log::info("Token refreshed successfully.");
} catch (\Exception $e) {
Log::error("Token refresh error: " . $e->getMessage());
throw new \Exception("Token refresh failed.");
// If refresh fails after retries, attempt re-login
$attempt = 0;
while ($attempt < $maxRetries) {
try {
$attempt++;
Log::info("Attempt $attempt: Re-logging in...");
$loginAgain = $this->service_token->get_token(); // Login again
if (!isset($loginAgain['access']) || !isset($loginAgain['refresh'])) {
throw new \Exception("Invalid login response.");
}
$this->user_token = $loginAgain['access'];
$this->user_refresh_token = $loginAgain['refresh'];
Log::info("Re-login successful on attempt $attempt.");
return; // Exit function on success
} catch (\Exception $e) {
Log::error("Re-login failed on attempt $attempt: " . $e->getMessage());
if ($attempt >= $maxRetries) {
throw new \Exception("Both token refresh and login failed after $maxRetries attempts. " . $e->getMessage());
}
sleep(30); // Wait for 30 seconds before retrying
}
}
}

View File

@@ -108,11 +108,4 @@ return [
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'failed_jobs',
],
// set timeout queue
'worker' => [
'timeout' => 40000
]
];

View File

@@ -0,0 +1,29 @@
<?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('import_datasources', function (Blueprint $table) {
$table->timestamp('start_time')->nullable();
$table->timestamp('finish_time')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('import_datasources', function (Blueprint $table) {
$table->dropColumn(['start_time', 'finish_time']);
});
}
};

View File

@@ -0,0 +1,28 @@
<?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('import_datasources', function (Blueprint $table) {
$table->string('failed_uuid')->nullable()->after('status');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('import_datasources', function (Blueprint $table) {
$table->dropColumn('failed_uuid');
});
}
};

View File

@@ -0,0 +1,32 @@
<?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::dropIfExists('pbg_task_attachments');
Schema::create('pbg_task_attachments', function (Blueprint $table) {
$table->id();
$table->foreignId('pbg_task_id')->constrained('pbg_task')->onDelete('cascade');
$table->string('file_name');
$table->string('file_path');
$table->string('pbg_type');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('pbg_task_attachments');
}
};

View File

@@ -15,23 +15,41 @@ class DatabaseSeeder extends Seeder
*/
public function run(): void
{
User::updateOrCreate(
['email' => 'user@demo.com'], // Kondisi pencarian
$users = [
[
'name' => 'Darkone',
'email' => 'user@sibedas.com',
'name' => 'User',
'email_verified_at' => now(),
'password' => Hash::make('password'),
'firstname' => 'John',
'lastname' => 'Doe',
'position' => 'crusial',
'firstname' => 'user',
'lastname' => 'user',
'position' => 'user',
'remember_token' => Str::random(10),
]
);
],
[
'email' => 'superadmin@sibedas.com',
'name' => 'Superadmin',
'email_verified_at' => now(),
'password' => Hash::make('password'),
'firstname' => 'superadmin',
'lastname' => 'superadmin',
'position' => 'superadmin',
'remember_token' => Str::random(10),
],
];
foreach ($users as $user) {
User::updateOrCreate(
['email' => $user['email']], // Search condition
$user // Update or create with this data
);
}
$this->call([
RoleSeeder::class,
MenuSeeder::class,
UsersRoleMenuSeeder::class
UsersRoleMenuSeeder::class,
GlobalSettingSeeder::class,
]);
}
}

View File

@@ -14,13 +14,20 @@ class MenuSeeder extends Seeder
*/
public function run(): void
{
$menus = [
$menus = [
[
"name" => "Neng Bedas",
"url" => "main-chatbot.index",
"icon" => "mingcute:wechat-line",
"parent_id" => null,
"sort_order" => 1,
],
[
"name" => "Dashboard",
"url" => "/dashboard",
"icon" => "mingcute:home-3-line",
"parent_id" => null,
"sort_order" => 1,
"sort_order" => 2,
"children" => [
[
"name" => "Dashboard Pimpinan",
@@ -67,7 +74,7 @@ class MenuSeeder extends Seeder
"url" => "/master",
"icon" => "mingcute:cylinder-line",
"parent_id" => null,
"sort_order" => 2,
"sort_order" => 3,
"children" => [
[
"name" => "Users",
@@ -82,7 +89,7 @@ class MenuSeeder extends Seeder
"url" => "/settings",
"icon" => "mingcute:settings-6-line",
"parent_id" => null,
"sort_order" => 3,
"sort_order" => 4,
"children" => [
[
"name" => "Syncronize",
@@ -109,7 +116,7 @@ class MenuSeeder extends Seeder
"url" => "/data-settings",
"icon" => "mingcute:settings-1-line",
"parent_id" => null,
"sort_order" => 4,
"sort_order" => 5,
"children" => [
[
"name" => "Setting Dashboard",
@@ -124,7 +131,7 @@ class MenuSeeder extends Seeder
"url" => "/data",
"icon" => "mingcute:task-line",
"parent_id" => null,
"sort_order" => 5,
"sort_order" => 6,
"children" => [
[
"name" => "PBG",
@@ -187,7 +194,7 @@ class MenuSeeder extends Seeder
"url" => "/laporan",
"icon" => "mingcute:report-line",
"parent_id" => null,
"sort_order" => 6,
"sort_order" => 7,
"children" => [
[
"name" => "Lap Pariwisata",
@@ -221,21 +228,6 @@ class MenuSeeder extends Seeder
],
]
],
[
"name" => "Neng Bedas",
"url" => "/chat",
"icon" => "mingcute:wechat-line",
"parent_id" => null,
"sort_order" => 7,
"children" => [
[
"name" => "Chat",
"url" => "main-chatbot.index",
"icon" => null,
"sort_order" => 1,
],
]
],
[
"name" => "Approval",
"url" => "/approval",

View File

@@ -18,10 +18,6 @@ class RoleSeeder extends Seeder
"name" => "superadmin",
"description" => "show all menus for super admins",
],
[
"name" => "admin",
"description" => "show only necessary menus for admins",
],
[
"name" => "operator",
"description" => "show only necessary menus for operators",

View File

@@ -16,7 +16,7 @@ class UsersRoleMenuSeeder extends Seeder
public function run(): void
{
// Fetch roles in a single query
$roles = Role::whereIn('name', ['superadmin', 'admin', 'operator'])->get()->keyBy('name');
$roles = Role::whereIn('name', ['superadmin', 'user', 'operator'])->get()->keyBy('name');
// Fetch all menus in a single query and index by name
$menus = Menu::whereIn('name', [
@@ -24,7 +24,7 @@ class UsersRoleMenuSeeder extends Seeder
'Approval', 'Tools', 'Dashboard Pimpinan', 'Dashboard PBG', 'Users', 'Syncronize',
'Menu', 'Role', 'Setting Dashboard', 'PBG', 'Reklame', 'Usaha atau Industri', 'Pariwisata',
'Lap Pariwisata', 'UMKM', 'Dashboard Potensi', 'Tata Ruang', 'PDAM', 'PETA',
'Lap Pimpinan', 'Chat', 'Dalam Sistem', 'Luar Sistem', 'Google Sheets', 'TPA TPT',
'Lap Pimpinan', 'Dalam Sistem', 'Luar Sistem', 'Google Sheets', 'TPA TPT',
'Approval Pejabat', 'Undangan', 'Rekap Pembayaran', 'Lap Rekap Data Pembayaran', 'Lap PBG (PTSP)'
])->get()->keyBy('name');
@@ -35,16 +35,21 @@ class UsersRoleMenuSeeder extends Seeder
'Approval', 'Tools', 'Dashboard Pimpinan', 'Dashboard PBG', 'Users', 'Syncronize',
'Menu', 'Role', 'Setting Dashboard', 'PBG', 'Reklame', 'Usaha atau Industri', 'Pariwisata',
'Lap Pariwisata', 'UMKM', 'Dashboard Potensi', 'Tata Ruang', 'PDAM', 'Dalam Sistem',
'Luar Sistem', 'Lap Pimpinan', 'Chat', 'Google Sheets', 'TPA TPT', 'Approval Pejabat',
'Luar Sistem', 'Lap Pimpinan', 'Google Sheets', 'TPA TPT', 'Approval Pejabat',
'Undangan', 'Rekap Pembayaran', 'Lap Rekap Data Pembayaran', 'Lap PBG (PTSP)'
],
'admin' => ['Dashboard', 'Master', 'Settings'],
'user' => ['Dashboard', 'Data', 'Laporan', 'Neng Bedas',
'Approval', 'Tools', 'Dashboard Pimpinan', 'Dashboard PBG', 'Users', 'Syncronize',
'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)'],
'operator' => ['Dashboard', 'Data', 'Laporan']
];
// Define permission levels
$superadminPermissions = ["allow_show" => true, "allow_create" => true, "allow_update" => true, "allow_destroy" => true];
$adminPermissions = ["allow_show" => true, "allow_create" => true, "allow_update" => true, "allow_destroy" => true];
$userPermissions = ["allow_show" => true, "allow_create" => false, "allow_update" => false, "allow_destroy" => false];
$operatorPermissions = ["allow_show" => true, "allow_create" => false, "allow_update" => false, "allow_destroy" => false];
// Assign menus to roles
@@ -54,13 +59,18 @@ class UsersRoleMenuSeeder extends Seeder
$role->menus()->sync(
collect($menuNames)->mapWithKeys(fn($menuName) => [
$menus[$menuName]->id => ($roleName === 'superadmin' ? $superadminPermissions :
($roleName === 'admin' ? $adminPermissions : $operatorPermissions))
($roleName === 'operator' ? $operatorPermissions : $userPermissions))
])->toArray()
);
}
}
// Attach User to role super admin
User::findOrFail(1)->roles()->sync([$roles['superadmin']->id]);
$accountSuperadmin = User::where('email', 'superadmin@sibedas.com')->first();
$accountUser = User::where('email', 'user@sibedas.com')->first();
$accountDefault = User::where('email','user@demo.com')->first();
$accountSuperadmin->roles()->sync([$roles['superadmin']->id]);
$accountUser->roles()->sync([$roles['user']->id]);
$accountDefault->roles()->sync([$roles['user']->id]);
}
}

View File

@@ -29,7 +29,11 @@ echo "🔄 Restarting PHP service..."
systemctl reload $PHP_VERSION-fpm
echo "🔁 Restarting Supervisor queue workers..."
supervisorctl reload
php artisan queue:restart
supervisorctl reread
supervisorctl update
supervisorctl restart all
php artisan up
echo "🚀 Deployment completed successfully!"

View File

@@ -27,6 +27,7 @@ class BigdataResume {
this.table = new Grid({
columns: [
{ name: "ID" },
{ name: "Year" },
{ name: "Jumlah Potensi" },
{ name: "Total Potensi" },
{ name: "Jumlah Berkas Belum Terverifikasi" },
@@ -79,6 +80,7 @@ class BigdataResume {
then: (data) => {
return data.data.map((item) => [
item.id,
item.year,
item.potention_count,
addThousandSeparators(item.potention_sum),
item.non_verified_count,

View File

@@ -41,8 +41,8 @@ class GoogleSheets {
tableContainer.innerHTML = "";
// Get user permissions from data attributes
let canUpdate = tableContainer.getAttribute("data-updater") === "1";
let canDelete = tableContainer.getAttribute("data-destroyer") === "1";
// let canUpdate = tableContainer.getAttribute("data-updater") === "1";
// let canDelete = tableContainer.getAttribute("data-destroyer") === "1";
this.table = new Grid({
columns: [
@@ -65,25 +65,25 @@ class GoogleSheets {
</a>
`;
if (canUpdate) {
buttons += `
<a href="#" class="btn btn-yellow btn-sm d-inline-flex align-items-center justify-content-center">
<i class='bx bx-edit'></i>
</a>
`;
}
// if (canUpdate) {
// buttons += `
// <a href="#" class="btn btn-yellow btn-sm d-inline-flex align-items-center justify-content-center">
// <i class='bx bx-edit'></i>
// </a>
// `;
// }
if (canDelete) {
buttons += `
<button data-id="${cell}" class="btn btn-sm btn-red btn-delete-google-sheet d-inline-flex align-items-center justify-content-center">
<i class='bx bxs-trash'></i>
</button>
`;
}
// if (canDelete) {
// buttons += `
// <button data-id="${cell}" class="btn btn-sm btn-red btn-delete-google-sheet d-inline-flex align-items-center justify-content-center">
// <i class='bx bxs-trash'></i>
// </button>
// `;
// }
if (!canUpdate && !canDelete) {
buttons = `<span class="text-muted">No Privilege</span>`;
}
// if (!canUpdate && !canDelete) {
// buttons = `<span class="text-muted">No Privilege</span>`;
// }
return gridjs.html(
`<div class="d-flex justify-content-center gap-2">${buttons}</div>`

View File

@@ -39,7 +39,7 @@ class Menus {
"Name",
"Url",
"Icon",
"ParentID",
"Parent Name",
"Sort Order",
{
name: "Action",
@@ -97,16 +97,22 @@ class Menus {
.getAttribute("content")}`,
"Content-Type": "application/json",
},
then: (data) =>
data.data.map((item) => [
item.id,
item.name,
item.url,
item.icon,
item.parent_id,
item.sort_order,
item.id,
]),
then: (data) => {
console.log("Full API Response:", data); // Log the full response
return data.data.map((item, index) => {
console.log(`Item ${index + 1}:`, item); // Log each item
return [
item.id,
item.name,
item.url,
item.icon,
item.parent?.name,
item.sort_order,
item.id,
];
});
},
total: (data) => data.total,
},
}).render(tableContainer);

View File

@@ -1,63 +1,124 @@
import { Grid } from "gridjs/dist/gridjs.umd.js";
import "gridjs/dist/gridjs.umd.js";
import gridjs from "gridjs/dist/gridjs.umd.js";
import { Grid, html } from "gridjs/dist/gridjs.umd.js";
import GlobalConfig from "../global-config";
import { Dropzone } from "dropzone";
Dropzone.autoDiscover = false;
class PbgTasks {
constructor() {
this.table = null;
this.toastMessage = document.getElementById("toast-message");
this.toastElement = document.getElementById("toastNotification");
}
init() {
this.setupFileUploadModal({
modalId: "modalBuktiBayar",
dropzoneId: "dropzoneBuktiBayar",
uploadBtnClass: "upload-btn-bukti-bayar",
removeBtnId: "removeFileBtnBuktiBayar",
submitBtnId: "submitBuktiBayar",
fileNameSpanId: "uploadedFileNameBuktiBayar",
fileInfoId: "fileInfoBuktiBayar",
pbgType: "bukti_bayar",
bindFlag: "uploadHandlerBoundBuktiBayar",
});
this.setupFileUploadModal({
modalId: "modalBeritaAcara",
dropzoneId: "dropzoneBeritaAcara",
uploadBtnClass: "upload-btn-berita-acara",
removeBtnId: "removeFileBtnBeritaAcara",
submitBtnId: "submitBeritaAcara",
fileNameSpanId: "uploadedFileNameBeritaAcara",
fileInfoId: "fileInfoBeritaAcara",
pbgType: "berita_acara",
bindFlag: "uploadHandlerBoundBeritaAcara",
});
this.initTableRequestAssignment();
this.handleSendNotification();
this.handleUpload();
this.handleUploadBeritaAcara();
}
initTableRequestAssignment() {
let tableContainer = document.getElementById("table-pbg-tasks");
// Ambil token
const token = document
.querySelector('meta[name="api-token"]')
.getAttribute("content");
// Pastikan kontainer kosong sebelum merender ulang Grid.js
tableContainer.innerHTML = "";
let canUpdate = tableContainer.getAttribute("data-updater") === "1";
new Grid({
const config = {
columns: [
"ID",
{ name: "Name", width: "15%" },
{ name: "Condition", width: "7%" },
{ name: "Name" },
{ name: "Condition" },
"Registration Number",
"Document Number",
{ name: "Address", width: "30%" },
{ name: "Address" },
"Status",
"Function Type",
"Consultation Type",
{ name: "Due Date", width: "10%" },
{ name: "Due Date" },
{
name: "Action",
formatter: (cell) => {
let tableContainer =
document.getElementById("table-pbg-tasks");
let canUpdate =
tableContainer.getAttribute("data-updater") === "1";
if (!canUpdate) {
return gridjs.html(
return html(
`<span class="text-muted">No Privilege</span>`
);
}
return gridjs.html(`
<div class="d-flex justify-content-center align-items-center gap-2">
<a href="/pbg-task/${cell}" class="btn btn-yellow btn-sm d-inline-flex align-items-center justify-content-center">
Detail
</a>
<button class="btn btn-sm btn-info upload-btn" data-id="${cell}">
Upload Bukti Bayar
</button>
<button class="btn btn-sm btn-info upload-btn-berita-acara" data-id="${cell}">
Buat Berita Acara
</button>
</div>
`);
return html(`
<div class="d-flex justify-content-center align-items-center gap-2">
<a href="/pbg-task/${cell.id}"
class="btn btn-yellow btn-sm d-inline-flex align-items-center justify-content-center"
style="white-space: nowrap; line-height: 1;">
<iconify-icon icon="mingcute:eye-2-fill" width="15" height="15" style="vertical-align: middle;"></iconify-icon>
</a>
${
cell.attachment_berita_acara
? `
<a href="/pbg-task-attachment/${cell.attachment_berita_acara.id}?type=berita-acara"
class="btn btn-success btn-sm d-inline-flex align-items-center justify-content-center"
style="white-space: nowrap; line-height: 1;"
target="_blank">
<iconify-icon icon="mingcute:eye-2-fill" width="15" height="15" style="vertical-align: middle;"></iconify-icon>
<span class="ms-1">Berita Acara</span>
</a>
`
: `
<button class="btn btn-sm btn-info d-inline-flex align-items-center justify-content-center upload-btn-berita-acara"
data-id="${cell.id}"
style="white-space: nowrap; line-height: 1;">
<iconify-icon icon="mingcute:upload-2-fill" width="15" height="15" style="vertical-align: middle;"></iconify-icon>
<span class="ms-1" style="line-height: 1;">Berita Acara</span>
</button>
`
}
${
cell.attachment_bukti_bayar
? `
<a href="/pbg-task-attachment/${cell.attachment_bukti_bayar.id}?type=bukti-bayar"
class="btn btn-success btn-sm d-inline-flex align-items-center justify-content-center"
style="white-space: nowrap; line-height: 1;"
target="_blank">
<iconify-icon icon="mingcute:eye-2-fill" width="15" height="15" style="vertical-align: middle;"></iconify-icon>
<span class="ms-1">Bukti Bayar</span>
</a>
`
: `
<button class="btn btn-sm btn-info d-inline-flex align-items-center justify-content-center upload-btn-bukti-bayar"
data-id="${cell.id}"
style="white-space: nowrap; line-height: 1;">
<iconify-icon icon="mingcute:upload-2-fill" width="15" height="15" style="vertical-align: middle;"></iconify-icon>
<span class="ms-1" style="line-height: 1;">Bukti Bayar</span>
</button>
`
}
</div>
`);
},
},
],
@@ -81,9 +142,7 @@ class PbgTasks {
url: `${GlobalConfig.apiHost}/api/request-assignments`,
credentials: "include",
headers: {
Authorization: `Bearer ${document
.querySelector('meta[name="api-token"]')
.getAttribute("content")}`,
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
then: (data) =>
@@ -98,11 +157,20 @@ class PbgTasks {
item.function_type,
item.consultation_type,
item.due_date,
item.id,
item,
]),
total: (data) => data.meta.total,
},
}).render(document.getElementById("table-pbg-tasks"));
};
const tableContainer = document.getElementById("table-pbg-tasks");
if (this.table) {
this.table.updateConfig(config).forceRender();
} else {
tableContainer.innerHTML = "";
this.table = new Grid(config).render(tableContainer);
}
}
handleSendNotification() {
@@ -128,70 +196,208 @@ class PbgTasks {
});
}
handleUpload() {
// Handle button click to show modal
document.addEventListener("click", function (event) {
if (event.target.classList.contains("upload-btn")) {
// Show modal
let uploadModal = new bootstrap.Modal(
document.getElementById("uploadModal")
);
uploadModal.show();
setupFileUploadModal({
modalId,
dropzoneId,
uploadBtnClass,
removeBtnId,
submitBtnId,
fileNameSpanId,
fileInfoId,
pbgType,
bindFlag,
}) {
const modalEl = document.getElementById(modalId);
const modalInstance = new bootstrap.Modal(modalEl);
let taskId;
// Blur-fix for modal
modalEl.addEventListener("hide.bs.modal", () => {
if (
document.activeElement &&
modalEl.contains(document.activeElement)
) {
document.activeElement.blur();
setTimeout(() => document.body.focus(), 10);
}
});
let dropzone = new Dropzone("#singleFileDropzone", {
url: "/upload-bukti-bayar", // Adjust to your backend endpoint
maxFiles: 1, // Allow only 1 file
maxFilesize: 5, // Limit size to 5MB
acceptedFiles: ".jpg,.png,.pdf", // Allowed file types
autoProcessQueue: false, // Prevent automatic upload
addRemoveLinks: true, // Show remove button
dictDefaultMessage: "Drop your file here or click to upload.",
init: function () {
let dz = this;
// Remove previous file when a new file is added
dz.on("addedfile", function () {
if (dz.files.length > 1) {
dz.removeFile(dz.files[0]);
}
});
// Bind click listener only once
if (!window[bindFlag]) {
document.addEventListener("click", (e) => {
const btn = e.target.closest(`.${uploadBtnClass}`);
if (btn) {
taskId = btn.getAttribute("data-id");
modalInstance.show();
}
});
window[bindFlag] = true;
}
// Handle upload button click
document
.getElementById("uploadBtn")
.addEventListener("click", function () {
if (dz.getQueuedFiles().length > 0) {
dz.processQueue(); // Manually process upload
} else {
alert("Please select a file to upload.");
}
// Avoid reinitializing Dropzone
if (!Dropzone.instances.some((dz) => dz.element.id === dropzoneId)) {
const self = this;
new Dropzone(`#${dropzoneId}`, {
url: () => `/api/pbg-task-attachment/${taskId}`,
maxFiles: 1,
maxFilesize: 5, // MB
acceptedFiles: ".jpg,.png,.pdf",
autoProcessQueue: false,
paramName: "file",
headers: {
"X-CSRF-TOKEN": document.querySelector(
'meta[name="csrf-token"]'
).content,
Authorization: `Bearer ${
document.querySelector('meta[name="api-token"]').content
}`,
Accept: "application/json",
},
params: { pbg_type: pbgType },
dictDefaultMessage: "Drop your file here or click to upload.",
init: function () {
const dz = this;
dz.on("addedfile", (file) => {
if (dz.files.length > 1) dz.removeFile(dz.files[0]);
setTimeout(() => {
document.getElementById(
fileNameSpanId
).textContent = file.name;
document
.getElementById(fileInfoId)
.classList.remove("d-none");
document
.querySelector(".dz-message")
.classList.add("d-none");
}, 10);
});
// Success callback
dz.on("success", function (file, response) {
alert("File uploaded successfully!");
dz.removeAllFiles(); // Clear after upload
});
dz.on("removedfile", () => {
document
.getElementById(fileInfoId)
.classList.add("d-none");
document.getElementById(fileNameSpanId).textContent =
"";
document
.querySelector(".dz-message")
.classList.remove("d-none");
});
// Error callback
dz.on("error", function (file, errorMessage) {
alert("Upload failed: " + errorMessage);
});
},
});
document
.getElementById(removeBtnId)
.addEventListener("click", () => dz.removeAllFiles());
document
.getElementById(submitBtnId)
.addEventListener("click", () => {
if (dz.getQueuedFiles().length > 0) {
dz.processQueue();
} else {
self.toastMessage.innerText =
"Please select a file to upload.";
self.toast.show();
}
});
dz.on("success", () => {
dz.removeAllFiles(true);
document
.getElementById(fileInfoId)
.classList.add("d-none");
document.getElementById(fileNameSpanId).textContent =
"";
document.querySelector(".dz-message").style.display =
"block";
document.activeElement.blur();
setTimeout(() => {
document.body.focus();
modalInstance.hide();
}, 50);
self.toastMessage.innerText =
"File uploaded successfully!";
self.toast.show();
self.initTableRequestAssignment();
});
dz.on("error", (file, message) => {
self.toastMessage.innerText =
message || "Upload failed!";
self.toast.show();
});
},
});
}
}
handleUploadBeritaAcara() {
// Handle button click to show modal
document.addEventListener("click", function (event) {
if (event.target.classList.contains("upload-btn-berita-acara")) {
// Show modal
let uploadModal = new bootstrap.Modal(
document.getElementById("uploadBeritaAcara")
);
uploadModal.show();
}
handleDownloadButtons(buttonClass) {
const buttons = document.querySelectorAll(`.${buttonClass}`);
buttons.forEach((button) => {
button.addEventListener("click", () => {
const attachmentId = button.getAttribute("data-id");
const originalContent = button.innerHTML;
// Disable button & show loading
button.disabled = true;
button.innerHTML = `
<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
Loading...
`;
fetch(`/api/pbg-task-attachment/${attachmentId}/download`, {
method: "GET",
headers: {
"X-CSRF-TOKEN": document.querySelector(
'meta[name="csrf-token"]'
).content,
Authorization: `Bearer ${document
.querySelector('meta[name="api-token"]')
.getAttribute("content")}`,
Accept: "application/json",
},
})
.then((response) => {
if (!response.ok) {
throw new Error("File not found or server error.");
}
return response
.blob()
.then((blob) => ({ blob, response }));
})
.then(({ blob, response }) => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
const contentDisposition = response.headers.get(
"Content-Disposition"
);
let fileName = "downloaded-file";
if (contentDisposition?.includes("filename=")) {
fileName = contentDisposition
.split("filename=")[1]
.replace(/"/g, "")
.trim();
}
a.download = fileName;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
})
.catch((error) => {
console.error("Download failed:", error);
alert("Failed to download file.");
})
.finally(() => {
button.disabled = false;
button.innerHTML = originalContent;
});
});
});
}
}

View File

@@ -2,10 +2,22 @@ import { Grid } from "gridjs/dist/gridjs.umd.js";
import "gridjs/dist/gridjs.umd.js";
import gridjs from "gridjs/dist/gridjs.umd.js";
import GlobalConfig from "../global-config";
import flatpickr from "flatpickr";
import "flatpickr/dist/flatpickr.min.css";
class PbgTaskAssignments {
init() {
this.initTablePbgTaskAssignments();
this.handleUpdateData();
this.initDatePicker();
}
initDatePicker() {
let element = document.getElementById("datepicker_due_date");
flatpickr(element, {
dateFormat: "Y-m-d",
minDate: new Date(),
});
}
initTablePbgTaskAssignments() {
@@ -62,6 +74,65 @@ class PbgTaskAssignments {
},
}).render(tableContainer);
}
handleUpdateData() {
const button = document.getElementById("btnUpdatePbgTask");
const form = document.getElementById("formUpdatePbgTask");
const toastNotification = document.getElementById("toastNotification");
const toast = new bootstrap.Toast(toastNotification);
button.addEventListener("click", function (event) {
event.preventDefault();
let submitButton = this;
let spinner = document.getElementById("spinner");
submitButton.disabled = true;
spinner.classList.remove("d-none");
const formData = new FormData(form);
const formObject = {};
formData.forEach((value, key) => {
formObject[key] = value;
});
fetch(form.action, {
method: "PUT", // Ensure your Laravel route is set to accept PUT requests
body: JSON.stringify(formObject), // Convert form data to JSON
credentials: "include",
headers: {
Authorization: `Bearer ${document
.querySelector('meta[name="api-token"]')
.getAttribute("content")}`,
"Content-Type": "application/json",
"X-CSRF-TOKEN": document.querySelector(
'meta[name="csrf-token"]'
).content, // For Laravel security
},
})
.then((response) => {
if (!response.ok) {
return response.json().then((err) => {
throw new Error(
err.message || "Something went wrong"
);
});
}
return response.json();
})
.then((data) => {
document.getElementById("toast-message").innerText =
data.message;
toast.show();
submitButton.disabled = false;
spinner.classList.add("d-none");
})
.catch((error) => {
console.error("Error updating task:", error);
document.getElementById("toast-message").innerText =
error.message;
toast.show();
submitButton.disabled = false;
spinner.classList.add("d-none");
});
});
}
}
document.addEventListener("DOMContentLoaded", function (e) {

View File

@@ -19,7 +19,32 @@ class SyncronizeTask {
"table-import-datasources"
);
this.table = new gridjs.Grid({
columns: ["ID", "Message", "Response", "Status", "Created"],
columns: [
"ID",
"Message",
"Response",
"Status",
"Started",
"Duration",
"Finished",
"Created",
{
name: "Action",
formatter: (cell) => {
if (
cell.status === "failed" &&
cell.failed_uuid !== null
) {
return gridjs.html(`
<button data-id="${cell.id}" class="btn btn-sm btn-warning d-flex align-items-center gap-1 btn-retry">
<iconify-icon icon="mingcute:refresh-3-line" width="15" height="15"></iconify-icon>
<span>Retry</span>
</button>
`);
}
},
},
],
search: {
server: {
url: (prev, keyword) => `${prev}?search=${keyword}`,
@@ -49,11 +74,24 @@ class SyncronizeTask {
item.message,
item.response_body,
item.status,
item.start_time,
item.duration,
item.finish_time,
item.created_at,
item,
]),
total: (data) => data.meta.total,
},
}).render(tableContainer);
tableContainer.addEventListener("click", (event) => {
let btn = event.target.closest(".btn-retry");
if (btn) {
const id = btn.getAttribute("data-id");
btn.disabled = true;
this.handleRetrySync(id, btn);
}
});
}
handleSubmitSync() {
const button = document.getElementById("btn-sync-submit");
@@ -105,6 +143,48 @@ class SyncronizeTask {
});
}
handleRetrySync(id, btn) {
const apiToken = document
.querySelector('meta[name="api-token"]')
.getAttribute("content");
fetch(`${GlobalConfig.apiHost}/api/retry-scraping/${id}`, {
method: "GET",
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
})
.then(async (response) => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log("API Response:", data); // Debugging
// Show success message
const message =
data?.data?.message ||
data?.message ||
"Synchronization successful!";
this.toastMessage.innerText = message;
this.toast.show();
})
.catch((err) => {
console.error("Fetch error:", err);
// Show error message
this.toastMessage.innerText =
err.message ||
"Failed to synchronize, something went wrong!";
this.toast.show();
// Re-enable button on failure
btn.disabled = false;
});
}
handleSyncClick() {
const button = document.getElementById("btn-sync-submit");
const spinner = document.getElementById("spinner");
@@ -148,8 +228,11 @@ class SyncronizeTask {
})
.catch((err) => {
console.error("Fetch error:", err);
alert("An error occurred during synchronization" + err.message);
this.toastMessage.innerText =
err.message || "Failed to syncronize something wrong!";
this.toast.show();
button.disabled = false;
spinner.classList.add("d-none");
});
}
}

View File

@@ -30,7 +30,7 @@ class="authentication-bg"
@if (sizeof($errors) > 0)
@foreach ($errors->all() as $error)
<p class="text-red-600 mb-3">{{ $error }}</p>
<p class="text-red mb-3">{{ $error }}</p>
@endforeach
@endif
@@ -39,6 +39,7 @@ class="authentication-bg"
<input type="email" class="form-control" id="email" name="email" placeholder="Enter your email">
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Enter your password">
</div>
<div class="d-grid">

View File

@@ -15,9 +15,9 @@
<div class="card w-100">
<div class="card-body">
<div class="d-flex flex-wrap justify-content-end align-items-center mb-2">
@if ($user_menu_permission['allow_create'])
<!-- @if ($user_menu_permission['allow_create'])
<a href="#" class="btn btn-success btn-sm d-block d-sm-inline w-auto">Create</a>
@endif
@endif -->
</div>
<div id="table-data-google-sheets"
data-updater="{{ $user_menu_permission['allow_update'] }}"

View File

@@ -0,0 +1,50 @@
@extends('layouts.vertical', ['subtitle' => $title])
@section('content')
@include('layouts.partials.page-title', ['title' => 'Data', 'subtitle' => 'PBG'])
<div class="row mb-4">
<div class="col-sm-12">
<div class="card border shadow-sm">
<div class="card-body">
<h5 class="mb-3">{{ $title }}</h5>
<p><strong>Document Number:</strong> {{ $pbg->document_number }}</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="card border shadow-sm">
<div class="card-body">
@php
$extension = strtolower(pathinfo($data->file_name, PATHINFO_EXTENSION));
@endphp
@if (in_array($extension, ['jpg', 'jpeg', 'png']))
<div class="text-center">
<img
src="{{ asset('storage/' . $data->file_path) }}"
alt="{{ $data->file_name }}"
class="img-fluid border rounded"
style="max-height: 600px;"
>
</div>
@elseif ($extension === 'pdf')
<iframe
src="{{ asset('storage/' . $data->file_path) }}"
width="100%"
height="700px"
style="border: none;"
></iframe>
@else
<div class="alert alert-warning">
Unsupported file type: <strong>{{ $extension }}</strong>
</div>
@endif
</div>
</div>
</div>
</div>
@endsection

View File

@@ -2,6 +2,26 @@
@section('css')
@vite(['node_modules/gridjs/dist/theme/mermaid.min.css'])
<style>
#dropzoneBuktiBayar .dz-preview{
display: none;
}
#dropzoneBeritaAcara .dz-preview{
display: none;
}
.file-info-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
background: rgba(255, 255, 255, 0.9);
padding: 0.75rem 1rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
}
</style>
@endsection
@section('content')
@@ -71,7 +91,7 @@
</div>
<!-- Modal -->
<div class="modal fade" id="uploadModal" tabindex="-1" aria-hidden="true">
<div class="modal fade" id="modalBuktiBayar" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
@@ -80,20 +100,25 @@
</div>
<div class="modal-body">
<div class="mb-3">
<form action="/upload-bukti-bayar" method="POST" class="dropzone" id="singleFileDropzone">
<div class="dz-message needsclick">
<i class="h1 bx bx-cloud-upload"></i>
<h3>Drop file here or click to upload.</h3>
<span class="text-muted fs-13">
(Only one file allowed. Selected file will be uploaded upon clicking submit.)
</span>
</div>
<form action="/upload-bukti-bayar" method="POST" class="dropzone" id="dropzoneBuktiBayar">
<div class="dz-message needsclick">
<i class="h1 bx bx-cloud-upload"></i>
<h3>Drop file here or click to upload.</h3>
<span class="text-muted fs-13">
(Only one file allowed. Selected file will be uploaded upon clicking submit.)
</span>
</div>
<!-- File info inside dropzone -->
<div id="fileInfoBuktiBayar" class="file-info-overlay d-none">
<span id="uploadedFileNameBuktiBayar" class="text-muted me-3"></span>
<button type="button" id="removeFileBtnBuktiBayar" class="btn btn-sm btn-danger">Hapus</button>
</div>
</form>
</div>
<!-- Submit Button -->
<div class="d-flex justify-content-end">
<button type="button" id="uploadBtn" class="btn btn-success">
<button type="button" id="submitBuktiBayar" class="btn btn-success">
<i class="bx bx-upload"></i> Upload
</button>
</div>
@@ -103,31 +128,36 @@
</div>
<!-- Modal -->
<div class="modal fade" id="uploadBeritaAcara" tabindex="-1" aria-hidden="true">
<div class="modal fade" id="modalBeritaAcara" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Upload Berita Acara</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<h5 class="modal-title">Upload Berita Acara</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<form action="/upload-berita-acara" method="POST" class="dropzone" id="singleFileDropzone">
<div class="dz-message needsclick">
<i class="h1 bx bx-cloud-upload"></i>
<h3>Drop file here or click to upload.</h3>
<span class="text-muted fs-13">
(Only one file allowed. Selected file will be uploaded upon clicking submit.)
</span>
</div>
</form>
<form action="/upload-berita-acara" method="POST" class="dropzone" id="dropzoneBeritaAcara">
<div class="dz-message needsclick">
<i class="h1 bx bx-cloud-upload"></i>
<h3>Drop file here or click to upload.</h3>
<span class="text-muted fs-13">
(Only one file allowed. Selected file will be uploaded upon clicking submit.)
</span>
</div>
<!-- File info inside dropzone -->
<div id="fileInfoBeritaAcara" class="file-info-overlay d-none">
<span id="uploadedFileNameBeritaAcara" class="text-muted me-3"></span>
<button type="button" id="removeFileBtnBeritaAcara" class="btn btn-sm btn-danger">Hapus</button>
</div>
</form>
</div>
<!-- Submit Button -->
<div class="d-flex justify-content-end">
<button type="button" id="uploadBeritaAcara" class="btn btn-success">
<i class="bx bx-upload"></i> Upload
</button>
<button type="button" id="submitBeritaAcara" class="btn btn-success">
<i class="bx bx-upload"></i> Upload
</button>
</div>
</div>
</div>

View File

@@ -7,69 +7,93 @@
@section('content')
@include('layouts.partials/page-title', ['title' => 'Data', 'subtitle' => 'PBG'])
<x-toast-notification />
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<dt>Name</dt>
<dd>{{$data->name}}</dd>
<form action="{{ route('api.pbg-task.update', ['task_uuid' => $data->uuid]) }}" id="formUpdatePbgTask">
@csrf
<div class="row mb-3">
<div class="col-md-6">
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" value="{{$data->name}}">
</div>
<div class="mb-3">
<label for="owner_name" class="form-label">Owner Name</label>
<input type="text" class="form-control" id="owner_name" name="owner_name" value="{{$data->owner_name}}">
</div>
<div class="mb-3">
<label for="application_type" class="form-label">Application Type Name</label>
<select name="application_type" class="form-select">
@foreach($applicationTypes as $key => $value)
<option value="{{ $key }}"
{{ (old('application_type', $data->application_type ?? '') == $key) ? 'selected' : '' }}>
{{ $value }}
</option>
@endforeach
</select>
</div>
<div class="mb-3">
<label for="condition" class="form-label">Condition</label>
<input type="text" class="form-control" id="condition" name="condition" value="{{$data->condition}}">
</div>
<div class="mb-3">
<label for="registration_number" class="form-label">Registration Number</label>
<input type="text" class="form-control" id="registration_number" name="registration_number" value="{{$data->registration_number}}">
</div>
<div class="mb-3">
<label for="document_number" class="form-label">Document Number</label>
<input type="text" class="form-control" id="document_number" name="document_number" value="{{$data->document_number}}">
</div>
<div class="mb-3">
<label for="status" class="form-label">Status Name</label>
<select name="status" class="form-select">
@foreach($statusOptions as $key => $value)
<option value="{{ $key }}" {{ old('status') == $key ? 'selected' : '' }}>
{{ $value }}
</option>
@endforeach
</select>
</div>
</div>
<div class="mb-3">
<dt>Owner Name</dt>
<dd>{{$data->owner_name}}</dd>
</div>
<div class="mb-3">
<dt>Aplication Type Name</dt>
<dd>{{$data->application_type_name}}</dd>
</div>
<div class="mb-3">
<dt>Condition</dt>
<dd>{{$data->condition}}</dd>
</div>
<div class="mb-3">
<dt>Registration Number</dt>
<dd>{{$data->registration_number}}</dd>
</div>
<div class="mb-3">
<dt>Document Number</dt>
<dd>{{$data->document_number}}</dd>
</div>
<div>
<dt>Status Name</dt>
<dd>{{$data->status_name}}</dd>
<div class="col-md-6">
<div class="mb-3">
<label for="address" class="form-label">Address</label>
<input type="text" class="form-control" id="address" name="address" value="{{$data->address}}">
</div>
<div class="mb-3">
<label for="slf_status_name" class="form-label">SLF Status Name</label>
<input type="text" class="form-control" id="slf_status_name" name="slf_status_name" value="{{$data->slf_status_name}}">
</div>
<div class="mb-3">
<label for="function_type" class="form-label">Function Type</label>
<input type="text" class="form-control" id="function_type" name="function_type" value="{{$data->function_type}}">
</div>
<div class="mb-3">
<label for="consultation_type" class="form-label">Consultation Type</label>
<input type="text" class="form-control" id="consultation_type" name="consultation_type" value="{{$data->consultation_type}}">
</div>
<div class="mb-3">
<label for="due_date" class="form-label">Due Date</label>
<input type="text" class="form-control" id="datepicker_due_date" name="due_date" value="{{$data->due_date}}">
</div>
<div>
<label for="task_created_at" class="form-label">Task Created At</label>
<input type="datetime-local" class="form-control" id="task_created_at" name="task_created_at" value="{{$data->task_created_at}}" disabled>
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<dt>Address</dt>
<dd>{{$data->address}}</dd>
</div>
<div class="mb-3">
<dt>SLF Status Name</dt>
<dd>{{$data->slf_status_name}}</dd>
</div>
<div class="mb-3">
<dt>Function Type</dt>
<dd>{{$data->function_type}}</dd>
</div>
<div class="mb-3">
<dt>Consultation Type</dt>
<dd>{{$data->consultation_type}}</dd>
</div>
<div class="mb-3">
<dt>Due Date</dt>
<dd>{{$data->due_date}}</dd>
</div>
<div>
<dt>Task Created At</dt>
<dd>{{$data->task_created_at}}</dd>
<div class="row">
<div class="d-flex justify-content-end">
<button type="button" id="btnUpdatePbgTask" class="btn btn-warning">
<span id="spinner" class="spinner-border spinner-border-sm me-1 d-none" role="status" aria-hidden="true"></span>
Update
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>

View File

@@ -10,6 +10,7 @@ use App\Http\Controllers\Api\GoogleSheetController;
use App\Http\Controllers\Api\ImportDatasourceController;
use App\Http\Controllers\Api\LackOfPotentialController;
use App\Http\Controllers\Api\MenusController;
use App\Http\Controllers\Api\PbgTaskAttachmentsController;
use App\Http\Controllers\Api\PbgTaskController;
use App\Http\Controllers\Api\PbgTaskGoogleSheetsController;
use App\Http\Controllers\Api\ReportPbgPtspController;
@@ -72,7 +73,11 @@ Route::group(['middleware' => 'auth:sanctum'], function (){
});
// scraping
Route::apiResource('/scraping', ScrapingController::class);
Route::controller(ScrapingController::class)->group(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);
@@ -105,9 +110,11 @@ Route::group(['middleware' => 'auth:sanctum'], function (){
});
Route::apiResource('/api-pbg-task', PbgTaskController::class);
Route::controller(PbgTaskController::class)->group( function (){
Route::put('/pbg-task/{task_uuid}/update', 'update')->name('api.pbg-task.update');
});
// sync pbg google sheet
Route::get('/sync-pbg-task-google-sheet', [PbgTaskController::class, 'syncPbgFromGoogleSheet'])->name('pbg-task.sync-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');
@@ -175,4 +182,9 @@ Route::group(['middleware' => 'auth:sanctum'], function (){
Route::get('/report-ptsp/excel', 'export_excel')->name('api.report-ptsp.excel');
Route::get('/report-ptsp/pdf', 'export_pdf')->name('api.report-ptsp.pdf');
});
Route::controller(PbgTaskAttachmentsController::class)->group(function (){
Route::post('/pbg-task-attachment/{pbg_task_id}', 'store')->name('api.pbg-task.upload');
Route::get('/pbg-task-attachment/{attachment_id}/download', 'download')->name('api.pbg-task.download');
});
});

View File

@@ -14,6 +14,7 @@ use App\Http\Controllers\InvitationsController;
use App\Http\Controllers\Master\UsersController;
use App\Http\Controllers\MenusController;
use App\Http\Controllers\PaymentRecapsController;
use App\Http\Controllers\PbgTaskAttachmentsController;
use App\Http\Controllers\ReportPaymentRecapsController;
use App\Http\Controllers\ReportPbgPTSPController;
use App\Http\Controllers\RequestAssignment\PbgTaskController;
@@ -64,6 +65,7 @@ Route::group(['middleware' => 'auth'], function(){
// data - PBG
Route::resource('/pbg-task', PbgTaskController::class);
Route::get('/pbg-task-attachment/{attachment_id}', [PbgTaskAttachmentsController::class, 'show'])->name('pbg-task-attachment.show');
// data settings
Route::resource('/data-settings', DataSettingController::class);

File diff suppressed because one or more lines are too long