Compare commits

..

14 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
38 changed files with 1318 additions and 971 deletions

View File

@@ -17,7 +17,7 @@ sudo nano /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker] [program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d 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 command=php /home/arifal/development/sibedas-pbg-web/artisan queue:work --queue=default --timeout=82800 --tries=1
autostart=true autostart=true
autorestart=true autorestart=true
numprocs=1 numprocs=1

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

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; namespace App\Http\Controllers\Api;
use App\Enums\ImportDatasourceStatus; use App\Enums\ImportDatasourceStatus;
use App\Enums\PbgTaskApplicationTypes;
use App\Enums\PbgTaskStatus;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\PbgTaskMultiStepRequest; use App\Http\Requests\PbgTaskMultiStepRequest;
use App\Http\Resources\PbgTaskResource; use App\Http\Resources\PbgTaskResource;
@@ -14,6 +16,7 @@ use App\Services\GoogleSheetService;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rules\Enum;
class PbgTaskController extends Controller class PbgTaskController extends Controller
{ {
@@ -116,9 +119,63 @@ class PbgTaskController extends Controller
/** /**
* Update the specified resource in storage. * 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) public function index(Request $request)
{ {
$query = PbgTask::query()->orderBy('id', 'desc'); $query = PbgTask::with(['attachments' => function ($q) {
if($request->has('search') && !empty($request->get("search"))){ $q->whereIn('pbg_type', ['berita_acara', 'bukti_bayar']);
$query->where('name', 'LIKE', '%'.$request->get('search').'%') }])->orderBy('id', 'desc');
->orWhere('registration_number', 'LIKE', '%'.$request->get('search').'%')
->orWhere('document_number', 'LIKE', '%'.$request->get('search').'%'); 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()); return RequestAssignmentResouce::collection($query->paginate());
} }

View File

@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api;
use App\Enums\ImportDatasourceStatus; use App\Enums\ImportDatasourceStatus;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Jobs\RetrySyncronizeJob;
use App\Jobs\ScrapingDataJob; use App\Jobs\ScrapingDataJob;
use App\Jobs\SyncronizeSIMBG; use App\Jobs\SyncronizeSIMBG;
use App\Models\ImportDatasource; use App\Models\ImportDatasource;
@@ -37,35 +38,25 @@ class ScrapingController extends Controller
return $this->resSuccess(["message" => "Success execute scraping service on background, check status for more"]); return $this->resSuccess(["message" => "Success execute scraping service on background, check status for more"]);
} }
/** public function retry_syncjob(string $import_datasource_id){
* Store a newly created resource in storage. try{
*/
public function store(Request $request)
{
//
}
/** $import_datasource = ImportDatasource::find($import_datasource_id);
* Display the specified resource. if(!$import_datasource){
*/ return $this->resError("Invalid import datasource id", null, 404);
public function show(string $id) }
{
//
}
/** dispatch(new RetrySyncronizeJob($import_datasource->id));
* Update the specified resource in storage. return response()->json([
*/ "success" => true,
public function update(Request $request, string $id) "message" => "Retrying scrape job on background, check status for more"
{ ]);
// }catch(\Exception $e){
} return response()->json([
"success" => false,
/** "message" => "Failed to retry sync job",
* Remove the specified resource from storage. "error" => $e->getMessage()
*/ ]);
public function destroy(string $id) }
{
//
} }
} }

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; namespace App\Http\Controllers\RequestAssignment;
use App\Enums\PbgTaskApplicationTypes;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\PbgTask; use App\Models\PbgTask;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use App\Enums\PbgTaskStatus;
class PbgTaskController extends Controller class PbgTaskController extends Controller
{ {
@@ -60,7 +62,9 @@ class PbgTaskController extends Controller
public function show(string $id) public function show(string $id)
{ {
$data = PbgTask::with(['pbg_task_retributions','pbg_task_index_integrations', 'pbg_task_retributions.pbg_task_prasarana'])->findOrFail($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

@@ -29,6 +29,7 @@ class ImportDatasourceResource extends JsonResource
"finish_time" => $finishTime ? $finishTime->toDateTimeString() : null, "finish_time" => $finishTime ? $finishTime->toDateTimeString() : null,
"created_at" => $this->created_at->toDateTimeString(), "created_at" => $this->created_at->toDateTimeString(),
"updated_at" => $this->updated_at->toDateTimeString(), "updated_at" => $this->updated_at->toDateTimeString(),
"failed_uuid" => $this->failed_uuid
]; ];
} }
} }

View File

@@ -34,6 +34,14 @@ class RequestAssignmentResouce extends JsonResource
'due_date' => $this->due_date, 'due_date' => $this->due_date,
'land_certificate_phase' => $this->land_certificate_phase, 'land_certificate_phase' => $this->land_certificate_phase,
'task_created_at' => $this->task_created_at, '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

@@ -2,6 +2,7 @@
namespace App\Jobs; namespace App\Jobs;
use App\Models\BigdataResume;
use App\Models\ImportDatasource; use App\Models\ImportDatasource;
use App\Services\ServiceGoogleSheet; use App\Services\ServiceGoogleSheet;
use App\Services\ServicePbgTask; use App\Services\ServicePbgTask;
@@ -43,13 +44,26 @@ class ScrapingDataJob implements ShouldQueue
'message' => 'Initiating scraping...', 'message' => 'Initiating scraping...',
'response_body' => null, 'response_body' => null,
'status' => 'processing', 'status' => 'processing',
'start_time' => now() 'start_time' => now(),
'failed_uuid' => null
]); ]);
$failed_uuid = null;
// Run the scraping services // Run the scraping services
$service_google_sheet->run_service(); $service_google_sheet->run_service();
$service_pbg_task->run_service(); $service_pbg_task->run_service();
$service_tab_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 // Update status to success
$import_datasource->update([ $import_datasource->update([
@@ -66,7 +80,8 @@ class ScrapingDataJob implements ShouldQueue
$import_datasource->update([ $import_datasource->update([
'status' => 'failed', 'status' => 'failed',
'response_body' => 'Error: ' . $e->getMessage(), 'response_body' => 'Error: ' . $e->getMessage(),
'finish_time' => now() 'finish_time' => now(),
'failed_uuid' => $failed_uuid,
]); ]);
} }

View File

@@ -15,6 +15,7 @@ class ImportDatasource extends Model
'response_body', 'response_body',
'status', 'status',
'start_time', 'start_time',
'finish_time' 'finish_time',
'failed_uuid'
]; ];
} }

View File

@@ -46,4 +46,8 @@ class PbgTask extends Model
{ {
return $this->hasMany(TaskAssignment::class, 'pbg_task_uid', 'uuid'); 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

@@ -27,6 +27,11 @@ class OpenAIService
return "Template prompt tidak ditemukan."; 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 // Ambil template berdasarkan kategori
$promptTemplate = $jsonData[$mainContent]['prompt']; $promptTemplate = $jsonData[$mainContent]['prompt'];
@@ -273,4 +278,26 @@ class OpenAIService
// return trim($response['choices'][0]['message']['content'] ?? 'No response'); // 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) { if (empty($sheet_data) || count($sheet_data) < 2) {
Log::warning("sync_google_sheet_data: No valid data found."); 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) { $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){ private function get_data_by_sheet($no_sheet = 1){
$spreadsheet = $this->service->spreadsheets->get($this->spreadsheetID); $spreadsheet = $this->service->spreadsheets->get($this->spreadsheetID);
$sheets = $spreadsheet->getSheets(); $sheets = $spreadsheet->getSheets();

View File

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

View File

@@ -19,6 +19,7 @@ class ServiceTabPbgTask
private $service_token; private $service_token;
private $user_token; private $user_token;
private $user_refresh_token; private $user_refresh_token;
protected $current_uuid = null;
public function __construct(Client $client, ServiceTokenSIMBG $service_token) public function __construct(Client $client, ServiceTokenSIMBG $service_token)
{ {
@@ -34,25 +35,42 @@ class ServiceTabPbgTask
$this->user_refresh_token = $auth_data['refresh']; $this->user_refresh_token = $auth_data['refresh'];
} }
public function run_service() public function run_service($retry_uuid = null)
{ {
try { try {
$pbg_tasks = PbgTask::all(); $pbg_tasks = PbgTask::orderBy('id')->get();
$start = false;
foreach ($pbg_tasks as $pbg_task) { foreach ($pbg_tasks as $pbg_task) {
$this->scraping_task_assignments($pbg_task->uuid); if($retry_uuid){
$this->scraping_task_retributions($pbg_task->uuid); if($pbg_task->uuid === $retry_uuid){
$this->scraping_task_integrations($pbg_task->uuid); $start = true;
}
// Process task assignments here if needed if(!$start){
Log::info("Successfully fetched for UUID: {$pbg_task->uuid}"); 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) { } catch (\Exception $e) {
Log::error("Failed to scrape task assignments: " . $e->getMessage()); Log::error("Failed to syncronize: " . $e->getMessage());
throw $e; throw $e;
} }
} }
public function getFailedUUID(){
return $this->current_uuid;
}
private function scraping_task_assignments($uuid) private function scraping_task_assignments($uuid)
{ {
$url = "{$this->simbg_host}/api/pbg/v1/list-tim-penilai/{$uuid}/?page=1&size=10"; $url = "{$this->simbg_host}/api/pbg/v1/list-tim-penilai/{$uuid}/?page=1&size=10";

View File

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

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 public function run(): void
{ {
User::updateOrCreate( $users = [
['email' => 'user@demo.com'], // Kondisi pencarian
[ [
'name' => 'Darkone', 'email' => 'user@sibedas.com',
'name' => 'User',
'email_verified_at' => now(), 'email_verified_at' => now(),
'password' => Hash::make('password'), 'password' => Hash::make('password'),
'firstname' => 'John', 'firstname' => 'user',
'lastname' => 'Doe', 'lastname' => 'user',
'position' => 'crusial', 'position' => 'user',
'remember_token' => Str::random(10), '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([ $this->call([
RoleSeeder::class, RoleSeeder::class,
MenuSeeder::class, MenuSeeder::class,
UsersRoleMenuSeeder::class UsersRoleMenuSeeder::class,
GlobalSettingSeeder::class,
]); ]);
} }
} }

View File

@@ -14,13 +14,20 @@ class MenuSeeder extends Seeder
*/ */
public function run(): void 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", "name" => "Dashboard",
"url" => "/dashboard", "url" => "/dashboard",
"icon" => "mingcute:home-3-line", "icon" => "mingcute:home-3-line",
"parent_id" => null, "parent_id" => null,
"sort_order" => 1, "sort_order" => 2,
"children" => [ "children" => [
[ [
"name" => "Dashboard Pimpinan", "name" => "Dashboard Pimpinan",
@@ -67,7 +74,7 @@ class MenuSeeder extends Seeder
"url" => "/master", "url" => "/master",
"icon" => "mingcute:cylinder-line", "icon" => "mingcute:cylinder-line",
"parent_id" => null, "parent_id" => null,
"sort_order" => 2, "sort_order" => 3,
"children" => [ "children" => [
[ [
"name" => "Users", "name" => "Users",
@@ -82,7 +89,7 @@ class MenuSeeder extends Seeder
"url" => "/settings", "url" => "/settings",
"icon" => "mingcute:settings-6-line", "icon" => "mingcute:settings-6-line",
"parent_id" => null, "parent_id" => null,
"sort_order" => 3, "sort_order" => 4,
"children" => [ "children" => [
[ [
"name" => "Syncronize", "name" => "Syncronize",
@@ -109,7 +116,7 @@ class MenuSeeder extends Seeder
"url" => "/data-settings", "url" => "/data-settings",
"icon" => "mingcute:settings-1-line", "icon" => "mingcute:settings-1-line",
"parent_id" => null, "parent_id" => null,
"sort_order" => 4, "sort_order" => 5,
"children" => [ "children" => [
[ [
"name" => "Setting Dashboard", "name" => "Setting Dashboard",
@@ -124,7 +131,7 @@ class MenuSeeder extends Seeder
"url" => "/data", "url" => "/data",
"icon" => "mingcute:task-line", "icon" => "mingcute:task-line",
"parent_id" => null, "parent_id" => null,
"sort_order" => 5, "sort_order" => 6,
"children" => [ "children" => [
[ [
"name" => "PBG", "name" => "PBG",
@@ -187,7 +194,7 @@ class MenuSeeder extends Seeder
"url" => "/laporan", "url" => "/laporan",
"icon" => "mingcute:report-line", "icon" => "mingcute:report-line",
"parent_id" => null, "parent_id" => null,
"sort_order" => 6, "sort_order" => 7,
"children" => [ "children" => [
[ [
"name" => "Lap Pariwisata", "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", "name" => "Approval",
"url" => "/approval", "url" => "/approval",

View File

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

View File

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

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

View File

@@ -1,63 +1,124 @@
import { Grid } from "gridjs/dist/gridjs.umd.js"; import { Grid, html } 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 GlobalConfig from "../global-config";
import { Dropzone } from "dropzone"; import { Dropzone } from "dropzone";
Dropzone.autoDiscover = false; Dropzone.autoDiscover = false;
class PbgTasks { class PbgTasks {
constructor() {
this.table = null;
this.toastMessage = document.getElementById("toast-message");
this.toastElement = document.getElementById("toastNotification");
}
init() { 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.initTableRequestAssignment();
this.handleSendNotification(); this.handleSendNotification();
this.handleUpload();
this.handleUploadBeritaAcara();
} }
initTableRequestAssignment() { 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 const config = {
tableContainer.innerHTML = "";
let canUpdate = tableContainer.getAttribute("data-updater") === "1";
new Grid({
columns: [ columns: [
"ID", "ID",
{ name: "Name", width: "15%" }, { name: "Name" },
{ name: "Condition", width: "7%" }, { name: "Condition" },
"Registration Number", "Registration Number",
"Document Number", "Document Number",
{ name: "Address", width: "30%" }, { name: "Address" },
"Status", "Status",
"Function Type", "Function Type",
"Consultation Type", "Consultation Type",
{ name: "Due Date", width: "10%" }, { name: "Due Date" },
{ {
name: "Action", name: "Action",
formatter: (cell) => { formatter: (cell) => {
let tableContainer =
document.getElementById("table-pbg-tasks");
let canUpdate = let canUpdate =
tableContainer.getAttribute("data-updater") === "1"; tableContainer.getAttribute("data-updater") === "1";
if (!canUpdate) { if (!canUpdate) {
return gridjs.html( return html(
`<span class="text-muted">No Privilege</span>` `<span class="text-muted">No Privilege</span>`
); );
} }
return gridjs.html(` return html(`
<div class="d-flex justify-content-center align-items-center gap-2"> <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"> <a href="/pbg-task/${cell.id}"
Detail class="btn btn-yellow btn-sm d-inline-flex align-items-center justify-content-center"
</a> style="white-space: nowrap; line-height: 1;">
<button class="btn btn-sm btn-info upload-btn" data-id="${cell}"> <iconify-icon icon="mingcute:eye-2-fill" width="15" height="15" style="vertical-align: middle;"></iconify-icon>
Upload Bukti Bayar </a>
</button>
<button class="btn btn-sm btn-info upload-btn-berita-acara" data-id="${cell}"> ${
Buat Berita Acara cell.attachment_berita_acara
</button> ? `
</div> <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`, url: `${GlobalConfig.apiHost}/api/request-assignments`,
credentials: "include", credentials: "include",
headers: { headers: {
Authorization: `Bearer ${document Authorization: `Bearer ${token}`,
.querySelector('meta[name="api-token"]')
.getAttribute("content")}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
then: (data) => then: (data) =>
@@ -98,11 +157,20 @@ class PbgTasks {
item.function_type, item.function_type,
item.consultation_type, item.consultation_type,
item.due_date, item.due_date,
item.id, item,
]), ]),
total: (data) => data.meta.total, 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() { handleSendNotification() {
@@ -128,70 +196,208 @@ class PbgTasks {
}); });
} }
handleUpload() { setupFileUploadModal({
// Handle button click to show modal modalId,
document.addEventListener("click", function (event) { dropzoneId,
if (event.target.classList.contains("upload-btn")) { uploadBtnClass,
// Show modal removeBtnId,
let uploadModal = new bootstrap.Modal( submitBtnId,
document.getElementById("uploadModal") fileNameSpanId,
); fileInfoId,
uploadModal.show(); 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 // Bind click listener only once
dz.on("addedfile", function () { if (!window[bindFlag]) {
if (dz.files.length > 1) { document.addEventListener("click", (e) => {
dz.removeFile(dz.files[0]); const btn = e.target.closest(`.${uploadBtnClass}`);
} if (btn) {
}); taskId = btn.getAttribute("data-id");
modalInstance.show();
}
});
window[bindFlag] = true;
}
// Handle upload button click // Avoid reinitializing Dropzone
document if (!Dropzone.instances.some((dz) => dz.element.id === dropzoneId)) {
.getElementById("uploadBtn") const self = this;
.addEventListener("click", function () {
if (dz.getQueuedFiles().length > 0) { new Dropzone(`#${dropzoneId}`, {
dz.processQueue(); // Manually process upload url: () => `/api/pbg-task-attachment/${taskId}`,
} else { maxFiles: 1,
alert("Please select a file to upload."); 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("removedfile", () => {
dz.on("success", function (file, response) { document
alert("File uploaded successfully!"); .getElementById(fileInfoId)
dz.removeAllFiles(); // Clear after upload .classList.add("d-none");
}); document.getElementById(fileNameSpanId).textContent =
"";
document
.querySelector(".dz-message")
.classList.remove("d-none");
});
// Error callback document
dz.on("error", function (file, errorMessage) { .getElementById(removeBtnId)
alert("Upload failed: " + errorMessage); .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() { handleDownloadButtons(buttonClass) {
// Handle button click to show modal const buttons = document.querySelectorAll(`.${buttonClass}`);
document.addEventListener("click", function (event) {
if (event.target.classList.contains("upload-btn-berita-acara")) { buttons.forEach((button) => {
// Show modal button.addEventListener("click", () => {
let uploadModal = new bootstrap.Modal( const attachmentId = button.getAttribute("data-id");
document.getElementById("uploadBeritaAcara") const originalContent = button.innerHTML;
);
uploadModal.show(); // 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/dist/gridjs.umd.js";
import gridjs from "gridjs/dist/gridjs.umd.js"; import gridjs from "gridjs/dist/gridjs.umd.js";
import GlobalConfig from "../global-config"; import GlobalConfig from "../global-config";
import flatpickr from "flatpickr";
import "flatpickr/dist/flatpickr.min.css";
class PbgTaskAssignments { class PbgTaskAssignments {
init() { init() {
this.initTablePbgTaskAssignments(); this.initTablePbgTaskAssignments();
this.handleUpdateData();
this.initDatePicker();
}
initDatePicker() {
let element = document.getElementById("datepicker_due_date");
flatpickr(element, {
dateFormat: "Y-m-d",
minDate: new Date(),
});
} }
initTablePbgTaskAssignments() { initTablePbgTaskAssignments() {
@@ -62,6 +74,65 @@ class PbgTaskAssignments {
}, },
}).render(tableContainer); }).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) { document.addEventListener("DOMContentLoaded", function (e) {

View File

@@ -28,6 +28,22 @@ class SyncronizeTask {
"Duration", "Duration",
"Finished", "Finished",
"Created", "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: { search: {
server: { server: {
@@ -62,10 +78,20 @@ class SyncronizeTask {
item.duration, item.duration,
item.finish_time, item.finish_time,
item.created_at, item.created_at,
item,
]), ]),
total: (data) => data.meta.total, total: (data) => data.meta.total,
}, },
}).render(tableContainer); }).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() { handleSubmitSync() {
const button = document.getElementById("btn-sync-submit"); const button = document.getElementById("btn-sync-submit");
@@ -117,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() { handleSyncClick() {
const button = document.getElementById("btn-sync-submit"); const button = document.getElementById("btn-sync-submit");
const spinner = document.getElementById("spinner"); const spinner = document.getElementById("spinner");

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') @section('css')
@vite(['node_modules/gridjs/dist/theme/mermaid.min.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 @endsection
@section('content') @section('content')
@@ -71,7 +91,7 @@
</div> </div>
<!-- Modal --> <!-- 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-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@@ -80,20 +100,25 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">
<form action="/upload-bukti-bayar" method="POST" class="dropzone" id="singleFileDropzone"> <form action="/upload-bukti-bayar" method="POST" class="dropzone" id="dropzoneBuktiBayar">
<div class="dz-message needsclick"> <div class="dz-message needsclick">
<i class="h1 bx bx-cloud-upload"></i> <i class="h1 bx bx-cloud-upload"></i>
<h3>Drop file here or click to upload.</h3> <h3>Drop file here or click to upload.</h3>
<span class="text-muted fs-13"> <span class="text-muted fs-13">
(Only one file allowed. Selected file will be uploaded upon clicking submit.) (Only one file allowed. Selected file will be uploaded upon clicking submit.)
</span> </span>
</div> </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> </form>
</div> </div>
<!-- Submit Button --> <!-- Submit Button -->
<div class="d-flex justify-content-end"> <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 <i class="bx bx-upload"></i> Upload
</button> </button>
</div> </div>
@@ -103,31 +128,36 @@
</div> </div>
<!-- Modal --> <!-- 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-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Upload Berita Acara</h5> <h5 class="modal-title">Upload Berita Acara</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">
<form action="/upload-berita-acara" method="POST" class="dropzone" id="singleFileDropzone"> <form action="/upload-berita-acara" method="POST" class="dropzone" id="dropzoneBeritaAcara">
<div class="dz-message needsclick"> <div class="dz-message needsclick">
<i class="h1 bx bx-cloud-upload"></i> <i class="h1 bx bx-cloud-upload"></i>
<h3>Drop file here or click to upload.</h3> <h3>Drop file here or click to upload.</h3>
<span class="text-muted fs-13"> <span class="text-muted fs-13">
(Only one file allowed. Selected file will be uploaded upon clicking submit.) (Only one file allowed. Selected file will be uploaded upon clicking submit.)
</span> </span>
</div> </div>
</form> <!-- 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> </div>
<!-- Submit Button --> <!-- Submit Button -->
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<button type="button" id="uploadBeritaAcara" class="btn btn-success"> <button type="button" id="submitBeritaAcara" class="btn btn-success">
<i class="bx bx-upload"></i> Upload <i class="bx bx-upload"></i> Upload
</button> </button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -7,69 +7,93 @@
@section('content') @section('content')
@include('layouts.partials/page-title', ['title' => 'Data', 'subtitle' => 'PBG']) @include('layouts.partials/page-title', ['title' => 'Data', 'subtitle' => 'PBG'])
<x-toast-notification />
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="row"> <form action="{{ route('api.pbg-task.update', ['task_uuid' => $data->uuid]) }}" id="formUpdatePbgTask">
<div class="col-md-6"> @csrf
<div class="mb-3"> <div class="row mb-3">
<dt>Name</dt> <div class="col-md-6">
<dd>{{$data->name}}</dd> <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>
<div class="mb-3"> <div class="col-md-6">
<dt>Owner Name</dt> <div class="mb-3">
<dd>{{$data->owner_name}}</dd> <label for="address" class="form-label">Address</label>
</div> <input type="text" class="form-control" id="address" name="address" value="{{$data->address}}">
<div class="mb-3"> </div>
<dt>Aplication Type Name</dt> <div class="mb-3">
<dd>{{$data->application_type_name}}</dd> <label for="slf_status_name" class="form-label">SLF Status Name</label>
</div> <input type="text" class="form-control" id="slf_status_name" name="slf_status_name" value="{{$data->slf_status_name}}">
<div class="mb-3"> </div>
<dt>Condition</dt> <div class="mb-3">
<dd>{{$data->condition}}</dd> <label for="function_type" class="form-label">Function Type</label>
</div> <input type="text" class="form-control" id="function_type" name="function_type" value="{{$data->function_type}}">
<div class="mb-3"> </div>
<dt>Registration Number</dt> <div class="mb-3">
<dd>{{$data->registration_number}}</dd> <label for="consultation_type" class="form-label">Consultation Type</label>
</div> <input type="text" class="form-control" id="consultation_type" name="consultation_type" value="{{$data->consultation_type}}">
<div class="mb-3"> </div>
<dt>Document Number</dt> <div class="mb-3">
<dd>{{$data->document_number}}</dd> <label for="due_date" class="form-label">Due Date</label>
</div> <input type="text" class="form-control" id="datepicker_due_date" name="due_date" value="{{$data->due_date}}">
<div> </div>
<dt>Status Name</dt> <div>
<dd>{{$data->status_name}}</dd> <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> </div>
<div class="col-md-6"> <div class="row">
<div class="mb-3"> <div class="d-flex justify-content-end">
<dt>Address</dt> <button type="button" id="btnUpdatePbgTask" class="btn btn-warning">
<dd>{{$data->address}}</dd> <span id="spinner" class="spinner-border spinner-border-sm me-1 d-none" role="status" aria-hidden="true"></span>
</div> Update
<div class="mb-3"> </button>
<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> </div>
</div> </div>
</div> </form>
</div> </div>
</div> </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\ImportDatasourceController;
use App\Http\Controllers\Api\LackOfPotentialController; use App\Http\Controllers\Api\LackOfPotentialController;
use App\Http\Controllers\Api\MenusController; use App\Http\Controllers\Api\MenusController;
use App\Http\Controllers\Api\PbgTaskAttachmentsController;
use App\Http\Controllers\Api\PbgTaskController; use App\Http\Controllers\Api\PbgTaskController;
use App\Http\Controllers\Api\PbgTaskGoogleSheetsController; use App\Http\Controllers\Api\PbgTaskGoogleSheetsController;
use App\Http\Controllers\Api\ReportPbgPtspController; use App\Http\Controllers\Api\ReportPbgPtspController;
@@ -72,7 +73,11 @@ Route::group(['middleware' => 'auth:sanctum'], function (){
}); });
// scraping // 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 // reklame
Route::apiResource('advertisements', AdvertisementController::class); Route::apiResource('advertisements', AdvertisementController::class);
@@ -105,9 +110,11 @@ Route::group(['middleware' => 'auth:sanctum'], function (){
}); });
Route::apiResource('/api-pbg-task', PbgTaskController::class); 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 // 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::apiResource('/api-google-sheet', GoogleSheetController::class);
Route::get('/sync-task', [SyncronizeController::class, 'syncPbgTask'])->name('api.task'); Route::get('/sync-task', [SyncronizeController::class, 'syncPbgTask'])->name('api.task');
Route::get('/get-user-token', [SyncronizeController::class, 'getUserToken'])->name('api.task.token'); Route::get('/get-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/excel', 'export_excel')->name('api.report-ptsp.excel');
Route::get('/report-ptsp/pdf', 'export_pdf')->name('api.report-ptsp.pdf'); 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\Master\UsersController;
use App\Http\Controllers\MenusController; use App\Http\Controllers\MenusController;
use App\Http\Controllers\PaymentRecapsController; use App\Http\Controllers\PaymentRecapsController;
use App\Http\Controllers\PbgTaskAttachmentsController;
use App\Http\Controllers\ReportPaymentRecapsController; use App\Http\Controllers\ReportPaymentRecapsController;
use App\Http\Controllers\ReportPbgPTSPController; use App\Http\Controllers\ReportPbgPTSPController;
use App\Http\Controllers\RequestAssignment\PbgTaskController; use App\Http\Controllers\RequestAssignment\PbgTaskController;
@@ -64,6 +65,7 @@ Route::group(['middleware' => 'auth'], function(){
// data - PBG // data - PBG
Route::resource('/pbg-task', PbgTaskController::class); Route::resource('/pbg-task', PbgTaskController::class);
Route::get('/pbg-task-attachment/{attachment_id}', [PbgTaskAttachmentsController::class, 'show'])->name('pbg-task-attachment.show');
// data settings // data settings
Route::resource('/data-settings', DataSettingController::class); Route::resource('/data-settings', DataSettingController::class);

File diff suppressed because one or more lines are too long