Compare commits

..

27 Commits

Author SHA1 Message Date
arifal
34e082c31b fix bug sync google sheet and handle to long data address on pbg task 2025-05-20 11:13:21 +07:00
arifal
c4d865bf2b fix get menu id and height auto chart growth 2025-05-15 18:09:28 +07:00
arifal
a103b38265 create page growth report 2025-05-15 17:02:02 +07:00
arifal
9aa3d32b6e add link in potential dashboard 2025-05-15 14:18:27 +07:00
arifal
99e2c214b6 create redirect link for dashboard pimpinan 2025-05-14 20:56:36 +07:00
arifal
501a76bc81 partial update circle chart can click 2025-05-06 17:29:39 +07:00
arifal
460267992e handle null value on detail quick search 2025-05-06 15:32:03 +07:00
arifal
becc368069 last update feat quick search 2025-05-06 15:06:59 +07:00
arifal
2618ac06d0 partial update create page quick search 2025-05-05 18:36:56 +07:00
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
75 changed files with 3084 additions and 1175 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,22 @@
<?php
namespace App\Enums;
enum PbgTaskFilterData : string
{
case non_business = 'non-business';
case business = 'business';
case verified = 'verified';
case non_verified = 'non-verified';
case all = 'all';
public static function getAllOptions() : array {
return [
self::all->value => 'Potensi Berkas',
self::business->value => 'Usaha',
self::non_business->value => 'Bukan Usaha',
self::verified->value => 'Terverifikasi',
self::non_verified->value => 'Belum Terverifikasi',
];
}
}

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

@@ -0,0 +1,85 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\BigdataResume;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class GrowthReportAPIController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
// Get current date
$today = Carbon::today();
// Define default range: 1 month back from today
$defaultStart = $today->copy()->subMonth();
$defaultEnd = $today;
// Use request values if provided, else use defaults
$startDate = $request->input('start_date', $defaultStart->toDateString());
$endDate = $request->input('end_date', $defaultEnd->toDateString());
// Optional year filter (used if specified)
$year = $request->input('year', now()->year);
$query = BigdataResume::selectRaw("
DATE(created_at) as date,
SUM(potention_sum) as potention_sum,
SUM(verified_sum) as verified_sum,
SUM(non_verified_sum) as non_verified_sum
")
->whereBetween('created_at', [$startDate, $endDate]);
$query->whereNotNull('year')
->where('year', '!=', 'all');
$data = $query->groupBy(DB::raw('DATE(created_at)'))
->orderBy(DB::raw('DATE(created_at)'))
->get()
->map(function ($item) {
$item->date = Carbon::parse($item->date)->format('d M Y');
return $item;
});
return response()->json($data);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(string $id)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
//
}
}

View File

@@ -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

@@ -8,7 +8,7 @@ use App\Http\Resources\RequestAssignmentResouce;
use App\Models\PbgTask;
use App\Models\PbgTaskGoogleSheet;
use Barryvdh\DomPDF\Facade\Pdf;
use DB;
use Illuminate\Support\Facades\DB;
use Exception;
use Illuminate\Http\Request;
use Log;
@@ -21,12 +21,51 @@ 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']);
},
'googleSheet'
])->orderBy('id', 'desc');
if ($request->has('filter') && !empty($request->get('filter'))) {
$filter = strtolower($request->get('filter'));
switch ($filter) {
case 'non-business':
$query->whereRaw("LOWER(function_type) != ?", ['sebagai tempat usaha'])->orWhereNull('function_type');
break;
case 'business':
$query->whereRaw("LOWER(function_type) = ?", ['sebagai tempat usaha']);
break;
case 'verified':
$query->whereHas('googleSheet', function ($q) {
$q->whereRaw("LOWER(status_verifikasi) = ?", ['selesai verifikasi']);
});
break;
case 'non-verified':
$query->where(function ($q) {
$q->whereDoesntHave('googleSheet')
->orWhereHas('googleSheet', function ($q2) {
$q2->whereRaw("LOWER(status_verifikasi) != ?", ['selesai verifikasi'])->orWhereNull('status_verifikasi');
});
});
break;
}
}
if ($request->has('search') && !empty($request->get("search"))) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
$q->where('name', 'LIKE', "%$search%")
->orWhere('registration_number', 'LIKE', "%$search%")
->orWhere('document_number', 'LIKE', "%$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

@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Dashboards;
use App\Http\Controllers\Controller;
use App\Models\ImportDatasource;
use App\Models\Menu;
use Illuminate\Http\Request;
class BigDataController extends Controller
@@ -12,7 +13,8 @@ class BigDataController extends Controller
$latest_import_datasource = ImportDatasource::latest()->first();
$latest_created = $latest_import_datasource ?
$latest_import_datasource->created_at->format("j F Y H:i:s") : null;
return view('dashboards.bigdata', compact('latest_created'));
$menus = Menu::all();
return view('dashboards.bigdata', compact('latest_created', 'menus'));
}
public function pbg()

View File

@@ -3,12 +3,14 @@
namespace App\Http\Controllers\Dashboards;
use App\Http\Controllers\Controller;
use App\Models\Menu;
use Illuminate\Http\Request;
class PotentialsController extends Controller
{
public function inside_system(){
return view('dashboards.potentials.inside_system');
$menus = Menu::all();
return view('dashboards.potentials.inside_system', compact('menus'));
}
public function outside_system(){
return view('dashboards.potentials.outside_system');

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

@@ -0,0 +1,87 @@
<?php
namespace App\Http\Controllers;
use App\Enums\PbgTaskApplicationTypes;
use App\Enums\PbgTaskStatus;
use App\Http\Resources\TaskAssignmentsResource;
use App\Models\PbgTask;
use App\Models\TaskAssignment;
use Illuminate\Http\Request;
class QuickSearchController extends Controller
{
public function index(){
return view("quick-search.index");
}
public function search_result(Request $request){
$keyword = $request->get("keyword");
return view('quick-search.result', compact('keyword'));
}
public function quick_search_datatable(Request $request)
{
try {
$query = PbgTask::orderBy('id', 'desc');
if ($request->filled('search')) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
$q->where('name', 'LIKE', "%$search%")
->orWhere('registration_number', 'LIKE', "%$search%")
->orWhere('address', 'LIKE', "%$search%")
->orWhere('document_number', 'LIKE', "%$search%");
});
}
return response()->json($query->paginate());
} catch (\Throwable $e) {
\Log::error("Error fetching datatable data: " . $e->getMessage());
return response()->json([
'message' => 'Terjadi kesalahan saat mengambil data.',
'error' => $e->getMessage(),
], 500);
}
}
public function show($id)
{
try {
$data = PbgTask::with([
'pbg_task_retributions',
'pbg_task_index_integrations',
'pbg_task_retributions.pbg_task_prasarana'
])->findOrFail($id);
$statusOptions = PbgTaskStatus::getStatuses();
$applicationTypes = PbgTaskApplicationTypes::labels();
return view("quick-search.detail", compact("data", 'statusOptions', 'applicationTypes'));
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
\Log::warning("PbgTask with ID {$id} not found.");
return redirect()->route('quick-search.index')->with('error', 'Data tidak ditemukan.');
} catch (\Throwable $e) {
\Log::error("Error in QuickSearchController@show: " . $e->getMessage());
return response()->view('pages.404', [], 500); // Optional: create `resources/views/errors/500.blade.php`
}
}
public function task_assignments(Request $request, $uuid){
try{
$query = TaskAssignment::query()
->where('pbg_task_uid', $uuid)
->orderBy('id', 'desc');
if ($request->filled('search')) {
$query->where('name', 'like', "%{$request->get('search')}%")
->orWhere('email', 'like', "%{$request->get('search')}%");
}
return TaskAssignmentsResource::collection($query->paginate(config('app.paginate_per_page', 50)));
}catch(\Exception $exception){
return response()->json(['message' => $exception->getMessage()], 500);
}
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Http\Controllers\Report;
use App\Http\Controllers\Controller;
use App\Models\Menu;
use Illuminate\Http\Request;
class GrowthReportsController extends Controller
{
public function index(){
return view('report.growth-report.index');
}
}

View File

@@ -2,11 +2,14 @@
namespace App\Http\Controllers\RequestAssignment;
use App\Enums\PbgTaskApplicationTypes;
use App\Enums\PbgTaskFilterData;
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
{
@@ -15,27 +18,21 @@ class PbgTaskController extends Controller
*/
public function index(Request $request)
{
$menuId = $request->query('menu_id');
$user = Auth::user();
$userId = $user->id;
$menuId = $request->query('menu_id') ?? $request->input('menu_id');
$filter = $request->query('filter');
// Ambil role_id yang dimiliki user
$roleIds = DB::table('user_role')
->where('user_id', $userId)
->pluck('role_id');
// Ambil data akses berdasarkan role_id dan menu_id
$roleAccess = DB::table('role_menu')
->whereIn('role_id', $roleIds)
->where('menu_id', $menuId)
->first();
// Pastikan roleAccess tidak null sebelum mengakses properti
$creator = $roleAccess->allow_create ?? 0;
$updater = $roleAccess->allow_update ?? 0;
$destroyer = $roleAccess->allow_destroy ?? 0;
return view('pbg_task.index', compact('creator', 'updater', 'destroyer'));
$permissions = $this->permissions[$menuId]?? []; // Avoid undefined index error
$creator = $permissions['allow_create'] ?? 0;
$updater = $permissions['allow_update'] ?? 0;
$destroyer = $permissions['allow_destroy'] ?? 0;
return view('pbg_task.index', [
'creator' => $creator,
'updater' => $updater,
'destroyer' => $destroyer,
'filter' => $filter,
'filterOptions' => PbgTaskFilterData::getAllOptions(),
]);
}
/**
@@ -60,7 +57,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' => 'Terjadi kesalahan, Syncronize tidak selesai',
'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) {
@@ -107,7 +107,7 @@ class ServiceGoogleSheet
'tanggal_skrd' => $this->convertToDate($cleanValue($row[33] ?? null)),
'ptsp' => $cleanValue($row[34] ?? null),
'selesai_terbit' => $cleanValue($row[35] ?? null),
'tanggal_pembayaran' => $cleanValue($row[36] ?? null),
'tanggal_pembayaran' => $this->convertToDate($cleanValue($row[36] ?? null)),
'format_sts' => $cleanValue($row[37] ?? null),
'tahun_terbit' => (int) $cleanValue($row[38] ?? null),
'tahun_berjalan' => (int) $cleanValue($row[39] ?? null),
@@ -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

@@ -13,13 +13,15 @@ class Circle extends Component
public $document_type;
public $document_id;
public $visible_small_circle;
public function __construct($document_id = "",$document_title = "", $document_type = "", $document_color = "#020c5c", $visible_small_circle = true)
public $document_url;
public function __construct($document_id = "",$document_title = "", $document_type = "", $document_color = "#020c5c", $visible_small_circle = true, $document_url = "#")
{
$this->document_title = $document_title;
$this->document_color = $document_color;
$this->document_type = $document_type;
$this->document_id = $document_id;
$this->visible_small_circle = $visible_small_circle;
$this->document_url = $document_url;
}
/**

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

@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('pbg_task', function(Blueprint $table){
$table->text('address')->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};

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",
@@ -201,38 +208,29 @@ class MenuSeeder extends Seeder
"icon" => null,
"sort_order" => 2,
],
[
"name" => "Lap Pertumbuhan",
"url" => "growths",
"icon" => null,
"sort_order" => 3,
],
[
"name" => "Rekap Pembayaran",
"url" => "payment-recaps",
"icon" => null,
"sort_order" => 3,
"sort_order" => 4,
],
[
"name" => "Lap Rekap Data Pembayaran",
"url" => "report-payment-recaps",
"icon" => null,
"sort_order" => 4,
"sort_order" => 5,
],
[
"name" => "Lap PBG (PTSP)",
"url" => "report-pbg-ptsp",
"icon" => null,
"sort_order" => 5,
],
]
],
[
"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,
"sort_order" => 6,
],
]
],

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,8 +24,8 @@ 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',
'Approval Pejabat', 'Undangan', 'Rekap Pembayaran', 'Lap Rekap Data Pembayaran', 'Lap PBG (PTSP)'
'Lap Pimpinan', 'Dalam Sistem', 'Luar Sistem', 'Google Sheets', 'TPA TPT',
'Approval Pejabat', 'Undangan', 'Rekap Pembayaran', 'Lap Rekap Data Pembayaran', 'Lap PBG (PTSP)', 'Lap Pertumbuhan'
])->get()->keyBy('name');
// Define access levels for each role
@@ -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',
'Undangan', 'Rekap Pembayaran', 'Lap Rekap Data Pembayaran', 'Lap PBG (PTSP)'
'Luar Sistem', 'Lap Pimpinan', 'Google Sheets', 'TPA TPT', 'Approval Pejabat',
'Undangan', 'Rekap Pembayaran', 'Lap Rekap Data Pembayaran', 'Lap PBG (PTSP)', 'Lap Pertumbuhan'
],
'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

@@ -168,6 +168,13 @@ document.addEventListener("DOMContentLoaded", async function (e) {
await new DashboardPotentialInsideSystem().init();
});
function handleCircleClick(element) {
const url = element.getAttribute("data-url") || "#";
if (url !== "#") {
window.location.href = url;
}
}
function resizeDashboard() {
let targetElement = document.getElementById("lack-of-potential-wrapper");
let dashboardElement = document.getElementById(

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,69 +1,164 @@
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 {
init() {
this.initTableRequestAssignment();
this.handleSendNotification();
this.handleUpload();
this.handleUploadBeritaAcara();
constructor() {
this.table = null;
this.toastMessage = document.getElementById("toast-message");
this.toastElement = document.getElementById("toastNotification");
}
initTableRequestAssignment() {
let tableContainer = document.getElementById("table-pbg-tasks");
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",
});
// Pastikan kontainer kosong sebelum merender ulang Grid.js
tableContainer.innerHTML = "";
let canUpdate = tableContainer.getAttribute("data-updater") === "1";
new Grid({
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.handleFilterDatatable();
this.handleSendNotification();
}
handleFilterDatatable() {
const form = document.getElementById("filter-form");
const filterSelect = document.getElementById("filter-select");
const urlParams = new URLSearchParams(window.location.search);
const initialFilter = urlParams.get("filter") || "";
this.initTableRequestAssignment(initialFilter); // Initial load with query param
form.addEventListener("submit", (e) => {
e.preventDefault();
const selectedFilter = filterSelect.value;
const params = new URLSearchParams(window.location.search);
params.set("filter", selectedFilter);
// Update the URL without reloading
window.history.replaceState(
{},
"",
`${location.pathname}?${params}`
);
// Call the method again with the selected filter
this.initTableRequestAssignment(selectedFilter);
});
}
initTableRequestAssignment(filterValue = "") {
const urlBase = `${GlobalConfig.apiHost}/api/request-assignments`;
// Ambil token
const token = document
.querySelector('meta[name="api-token"]')
.getAttribute("content");
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>
`);
},
},
],
search: {
server: {
url: (prev, keyword) => `${prev}?search=${keyword}`,
url: (prev, keyword) =>
`${prev}${
prev.includes("?") ? "&" : "?"
}search=${keyword}`,
},
debounceTimeout: 1000,
},
@@ -78,12 +173,10 @@ class PbgTasks {
},
sort: true,
server: {
url: `${GlobalConfig.apiHost}/api/request-assignments`,
url: `${urlBase}?filter=${filterValue}`,
credentials: "include",
headers: {
Authorization: `Bearer ${document
.querySelector('meta[name="api-token"]')
.getAttribute("content")}`,
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
then: (data) =>
@@ -98,11 +191,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 +230,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

@@ -0,0 +1,70 @@
import { Grid } from "gridjs";
class QuickSearchDetail {
init() {
this.initTablePbgTaskAssignments();
}
initTablePbgTaskAssignments() {
let tableContainer = document.getElementById(
"table-pbg-task-assignments"
);
let url_task_assignments = document.getElementById(
"url_task_assignments"
).value;
new Grid({
columns: [
"ID",
"Nama",
"Email",
"Nomor Telepon",
"Keahlian",
"Status",
],
search: {
server: {
url: (prev, keyword) => `${prev}?search=${keyword}`,
},
debounceTimeout: 1000,
},
pagination: {
limit: 15,
server: {
url: (prev, page) =>
`${prev}${prev.includes("?") ? "&" : "?"}page=${
page + 1
}`,
},
},
sort: true,
server: {
url: `${url_task_assignments}`,
then: (data) => {
return data.data.map((item) => {
const expertiseArray =
typeof item.expertise === "string"
? JSON.parse(item.expertise)
: item.expertise;
return [
item.id,
item.name,
item.email,
item.phone_number,
Array.isArray(expertiseArray)
? expertiseArray.map((e) => e.name).join(", ")
: "-",
item.status_name,
];
});
},
total: (data) => data.meta.total,
},
}).render(tableContainer);
}
}
document.addEventListener("DOMContentLoaded", function (e) {
new QuickSearchDetail().init();
});

View File

@@ -0,0 +1,21 @@
document.addEventListener("DOMContentLoaded", function () {
const searchBtn = document.getElementById("searchBtn");
const searchInput = document.getElementById("searchInput");
searchBtn.addEventListener("click", function () {
const keyword = searchInput.value.trim();
if (keyword !== "") {
// Redirect to the route with query parameter
window.location.href = `/search-result?keyword=${encodeURIComponent(
keyword
)}`;
}
});
// Optional: trigger search on Enter key
searchInput.addEventListener("keydown", function (e) {
if (e.key === "Enter") {
searchBtn.click();
}
});
});

View File

@@ -0,0 +1,135 @@
import { Grid, html } from "gridjs";
class QuickSearchResult {
constructor() {
this.table = null;
const baseInput = document.getElementById("base_url_datatable");
this.baseUrl = baseInput ? baseInput.value.split("?")[0] : "";
this.keywordInput = document.getElementById("search_input");
this.searchButton = document.getElementById("search_button");
this.datatableUrl = this.buildUrl(this.keywordInput.value);
}
init() {
this.bindSearchButton();
this.initDatatable();
}
bindSearchButton() {
const handleSearch = () => {
const newKeyword = this.keywordInput.value.trim();
if (newKeyword !== "") {
// 1. Update datatable URL and reload
this.datatableUrl = this.buildUrl(newKeyword);
this.initDatatable();
// 2. Update URL query string (without reloading the page)
const newUrl = `${
window.location.pathname
}?keyword=${encodeURIComponent(newKeyword)}`;
window.history.pushState({ path: newUrl }, "", newUrl);
// 3. Update visible keyword text in <em>{{ $keyword }}</em>>
const keywordDisplay = document.querySelector(".qs-header em");
if (keywordDisplay) {
keywordDisplay.textContent = newKeyword;
}
}
};
this.searchButton.addEventListener("click", handleSearch);
this.keywordInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
handleSearch();
}
});
}
buildUrl(keyword) {
const url = new URL(this.baseUrl, window.location.origin);
url.searchParams.set("search", keyword);
return url.toString();
}
initDatatable() {
const tableContainer = document.getElementById(
"datatable-quick-search-result"
);
const config = {
columns: [
"ID",
{ name: "Name" },
{ name: "Condition" },
"Registration Number",
"Document Number",
{ name: "Address" },
"Status",
"Function Type",
"Consultation Type",
{ name: "Due Date" },
{
name: "Action",
formatter: (cell) => {
return html(`
<a href="/quick-search/${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>
`);
},
},
],
search: false,
pagination: {
limit: 15,
server: {
url: (prev, page) =>
`${prev}${prev.includes("?") ? "&" : "?"}page=${
page + 1
}`,
},
},
sort: true,
server: {
url: this.datatableUrl,
then: (data) =>
data.data.map((item) => [
item.id,
item.name,
item.condition,
item.registration_number,
item.document_number,
item.address,
item.status_name,
item.function_type,
item.consultation_type,
item.due_date,
item,
]),
total: (data) => data.total,
},
};
if (this.table) {
this.table
.updateConfig({
...config,
server: { ...config.server, url: this.datatableUrl },
})
.forceRender();
} else {
tableContainer.innerHTML = "";
this.table = new Grid(config).render(tableContainer);
}
}
}
document.addEventListener("DOMContentLoaded", function () {
const app = new QuickSearchResult();
app.init();
});

View File

@@ -0,0 +1,94 @@
import ApexCharts from "apexcharts";
class GrowthReport {
init() {
this.loadChart();
}
async loadChart() {
try {
const chartElement = document.getElementById("chart-growth-report");
const apiUrl = chartElement.dataset.url;
const token = document
.querySelector('meta[name="api-token"]')
.getAttribute("content");
const response = await fetch(apiUrl, {
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
});
const data = await response.json();
console.log("data", data);
const categories = data.map((item) => item.date);
const potentionSeries = {
name: "Potensi Berkas",
data: data.map((item) => item.potention_sum),
};
const verifiedSeries = {
name: "Terverifikasi",
data: data.map((item) => item.verified_sum),
};
const nonVerifiedSeries = {
name: "Belum Terverifikasi",
data: data.map((item) => item.non_verified_sum),
};
const options = {
chart: {
type: "bar",
height: "auto",
},
title: {
text: "Grafik Pertumbuhan",
},
dataLabels: {
enabled: false,
},
legend: {
show: true,
},
xaxis: {
categories: categories,
},
yaxis: {
title: {
text: "Total SUM Per Date",
},
labels: {
formatter: function (value) {
return "Rp. " + value.toLocaleString("id-ID");
},
},
},
noData: {
text: "Data tidak tersedia",
align: "center",
verticalAlign: "middle",
style: {
color: "#999",
fontSize: "16px",
},
},
series: [potentionSeries, verifiedSeries, nonVerifiedSeries],
};
const chart = new ApexCharts(chartElement, options);
chart.render();
} catch (error) {
console.error("Failed to load growth report data:", error);
}
}
}
document.addEventListener("DOMContentLoaded", function () {
new GrowthReport().init();
});

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

@@ -0,0 +1,73 @@
.qs-detail-container {
color: #000; // black text
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
.card {
background-color: #fff;
.card-header {
background-color: #f5f5f5;
font-weight: bold;
color: #000;
}
.card-body {
dt {
font-weight: 600;
color: #000;
}
dd {
margin-bottom: 10px;
color: #000;
}
.nav-tabs {
border-bottom: 1px solid #000;
.nav-link {
color: #000;
border: 1px solid transparent;
border-top-left-radius: 0.25rem;
border-top-right-radius: 0.25rem;
font-weight: 500;
&.active {
background-color: #e0e0e0;
border-color: #000 #000 #fff;
}
&:hover {
color: #000;
}
}
}
.tab-content {
padding: 1rem;
}
.mb-3 {
dt {
font-weight: bold;
}
dd {
margin-left: 0;
}
}
.border {
border-color: #000 !important;
}
.shadow-sm {
box-shadow: none !important;
}
.rounded {
border-radius: 4px !important;
}
}
}
}

View File

@@ -0,0 +1,78 @@
.gsp-body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh; // ensures full vertical centering
background: #f0f2f5;
}
.gsp-icon {
width: 180px; // slightly smaller for better mobile view
height: auto;
margin-bottom: 15px;
}
.gsp-title {
font-size: 36px;
font-weight: 700;
color: #333;
}
.gsp-input {
width: 100%;
height: 44px;
font-size: 16px;
padding: 0 20px;
border-radius: 24px;
border: 1px solid #dfe1e5;
background-color: #fff;
box-shadow: none;
transition: box-shadow 0.2s ease-in-out, border-color 0.2s;
&:focus {
border-color: transparent;
box-shadow: 0 1px 6px rgba(32, 33, 36, 0.28);
outline: none;
}
}
.gsp-btn {
height: 44px;
padding: 0 24px;
font-size: 16px;
border: none;
border-radius: 24px;
background-color: #1a73e8;
color: white;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
&:hover {
background-color: #1558d6;
}
}
@media (max-width: 576px) {
.gsp-input-wrapper {
flex-direction: column;
align-items: stretch;
}
.gsp-btn {
width: 100%;
padding: 0 20px;
}
.gsp-input {
padding: 0 20px;
font-size: 16px;
}
.gsp-title {
font-size: 28px;
}
.gsp-icon {
width: 140px;
}
}

View File

@@ -0,0 +1,130 @@
.qs-wrapper {
width: 100%;
margin: 0 auto;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
color: #2c3e50;
}
.qs-toolbar {
border-bottom: 1px solid #e0e0e0;
margin-bottom: 1.5rem;
}
.qs-search-form {
width: 100%;
.gsp-input {
width: 100%;
height: 44px;
font-size: 16px;
padding: 0 20px;
border-radius: 24px;
border: 1px solid #dfe1e5;
background-color: #fff;
box-shadow: none;
transition: box-shadow 0.2s ease-in-out, border-color 0.2s;
&:focus {
border-color: transparent;
box-shadow: 0 1px 6px rgba(32, 33, 36, 0.28);
outline: none;
}
}
.gsp-btn {
height: 44px;
padding: 0 24px;
font-size: 16px;
border: none;
border-radius: 24px;
background-color: #1a73e8;
color: white;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
&:hover {
background-color: #1558d6;
}
}
}
.qs-header {
margin-bottom: 30px;
text-align: center;
h2 {
font-size: 24px;
font-weight: 600;
color: #1a237e;
em {
font-style: normal;
color: #0d47a1;
}
}
p {
font-size: 16px;
color: #555;
}
}
.qs-table-wrapper {
background-color: #fff;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
overflow-x: auto; // allow horizontal scroll on small screens
}
/* Grid.js overrides */
.qs-table-wrapper .gridjs {
font-size: 14px;
color: #333;
}
.qs-table-wrapper .gridjs-table {
width: 100%;
border-collapse: collapse;
}
.qs-table-wrapper .gridjs-th,
.qs-table-wrapper .gridjs-td {
padding: 12px 16px;
border: 1px solid #e0e0e0;
text-align: left;
vertical-align: middle;
}
.qs-table-wrapper .gridjs-th {
background-color: #f5f5f5;
font-weight: 600;
color: #1b1b1b;
}
.qs-table-wrapper .gridjs-tr:hover {
background-color: #f9f9f9;
}
.qs-table-wrapper .gridjs-pagination {
margin-top: 16px;
justify-content: center;
}
@media (max-width: 768px) {
.qs-header h2 {
font-size: 20px;
}
.qs-wrapper {
padding: 20px 10px;
}
.qs-table-wrapper {
padding: 15px;
}
.qs-table-wrapper .gridjs-th,
.qs-table-wrapper .gridjs-td {
padding: 10px 12px;
font-size: 13px;
}
}

View File

@@ -21,8 +21,8 @@ class="authentication-bg"
<img src="/images/dputr-kab-bandung.png" height="auto" width="100%" alt="logo light">
</a>
</div>
<h4 class="fw-bold text-dark mb-2">Welcome Back!</h4>
<p class="text-muted">Sign in to your account to continue</p>
<h4 class="fw-bold text-dark mb-2">Selamat Datang!</h4>
<p class="text-muted">Masuk kedalam akun untuk melihat lebih lanjut</p>
</div>
<form method="POST" action="{{ route('login') }}" class="mt-4">
@@ -30,20 +30,26 @@ 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
<div class="mb-3">
<label for="email" class="form-label">Email Address</label>
<label for="email" class="form-label">Email </label>
<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">
<button class="btn btn-dark btn-lg fw-medium" type="submit">Sign In</button>
</div>
<div class="d-flex justify-content-start mt-3">
<a href="{{ route('search') }}" class="">
Pencarian cepat
</a>
</div>
</form>
</div>
</div>

View File

@@ -1,8 +1,11 @@
@props(['document_url' => '#'])
@section('css')
@vite(['resources/scss/components/_circle.scss'])
@endsection
<div class="circle-container" id="{{$document_id}}" style="--circle-color: {{$document_color}};{{$style}}">
<div class="circle-container" id="{{$document_id}}" style="--circle-color: {{$document_color}};{{$style}}" data-url="{{ $document_url }}"
onclick="handleCircleClick(this)">
<div class="circle-content">
<p class="document-title {{$document_id}}" >{{$document_title}}</p>
<p class="document-total {{$document_id}}" >Rp.0</p>
@@ -17,3 +20,12 @@
</div>
@endif
</div>
<script>
function handleCircleClick(element) {
const url = element.getAttribute('data-url') || '#';
if (url !== '#') {
window.location.href = url;
}
}
</script>

View File

@@ -1,11 +1,11 @@
@props(['title' => 'title component', 'visible_data' => false, 'data_count' => '', 'visible_data_type' => false,
'data_type' => '','style' => '', 'size' => '', 'line' => [], 'data_id' => ''])
'data_type' => '','style' => '', 'size' => '', 'data_id' => '', 'document_url' => '#'])
@section('css')
@vite(['resources/scss/components/_custom_circle.scss'])
@endsection
<div class="custom-circle-wrapper {{ $size }}" style="{{ $style }}">
<div class="custom-circle-wrapper {{ $size }}" style="{{ $style }}" data-url="{{ $document_url }}" onclick="handleCircleClick(this)">
<div class="custom-circle-content">
<p class="custom-circle-text">{{ $title }}</p>
@if ($visible_data === "true")
@@ -14,17 +14,5 @@
@if ($visible_data_type === "true")
<div class="custom-circle-data-type">{{ $data_type }}</div>
@endif
@if (!empty($lines))
<svg class="absolute w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
@foreach ($lines as $line)
<line
x1="{{ $line['x1'] }}" y1="{{ $line['y1'] }}"
x2="{{ $line['x2'] }}" y2="{{ $line['y2'] }}"
stroke="{{ $line['color'] ?? 'black' }}"
stroke-width="{{ $line['width'] ?? 2 }}"
/>
@endforeach
</svg>
@endif
</div>
</div>

View File

@@ -9,22 +9,6 @@
@include('layouts.partials/page-title', ['title' => 'Dashboards', 'subtitle' => 'Dashboard Pimpinan'])
<div id="dashboard-fixed-wrapper" class="row">
<!-- <div class="col-12">
<h2 class="mt-3 ms-2 text-danger">
<span class="float-end fs-6 me-3 text-black d-block d-sm-inline text-end">Terakhir di update - {{$latest_created}}</span>
ANALISA BIG DATA PROSES PBG <br>
MELALUI APLIKASI SIBEDAS PBG
</h2>
</div>
<div class="row d-flex justify-content-end">
<div class="col-12 col-sm-6 col-md-3">
<div class="d-flex flex-sm-nowrap flex-wrap justify-content-end">
<input type="text" class="form-control" style="max-width: 125px;" id="datepicker-dashboard-bigdata" placeholder="Filter Date" />
</div>
</div>
</div> -->
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mt-3 ms-2">
<h2 class="text-danger m-0">
@@ -54,7 +38,8 @@
'document_type' => '',
'document_id' => 'chart-target-pad',
'visible_small_circle' => true,
'style' => 'left:200px;'
'style' => 'left:200px;',
'document_url' => route('data-settings.index', ['menu_id' => $menus->where('url','data-settings.index')->first()->id])
])
@endcomponent
@@ -72,7 +57,8 @@
'document_type' => 'Pemohon',
'document_id' => 'chart-total-potensi',
'visible_small_circle' => true,
'style' => 'left:400px;top:150px;'
'style' => 'left:400px;top:150px;',
'document_url' => route('pbg-task.index', ['menu_id' => $menus->where('url','pbg-task.index')->first()->id, 'filter' => 'all'])
])
@endcomponent
@@ -88,7 +74,8 @@
'document_type' => '',
'document_id' => 'chart-potensi-tata-ruang',
'visible_small_circle' => true,
'style' => 'left:600px;'
'style' => 'left:600px;',
'document_url' => route('web-spatial-plannings.index', ['menu_id' => $menus->where('url','web-spatial-plannings.index')->first()->id])
])
@endcomponent
@@ -101,7 +88,8 @@
'document_type' => 'Berkas',
'document_id' => 'chart-non-business',
'visible_small_circle' => true,
'style' => 'left:900px;top:150px;'
'style' => 'left:900px;top:150px;',
'document_url' => route('pbg-task.index', ['menu_id' => $menus->where('url','pbg-task.index')->first()->id, 'filter' => 'non-business'])
])
@endcomponent
@@ -111,7 +99,8 @@
'document_type' => 'Berkas',
'document_id' => 'chart-business',
'visible_small_circle' => true,
'style' => 'left:900px;top:400px;'
'style' => 'left:900px;top:400px;',
'document_url' => route('pbg-task.index', ['menu_id' => $menus->where('url','pbg-task.index')->first()->id, 'filter' => 'business'])
])
@endcomponent
@@ -121,7 +110,8 @@
'document_type' => 'Berkas',
'document_id' => 'chart-berkas-terverifikasi',
'visible_small_circle' => true,
'style' => 'top:300px;left:200px;'
'style' => 'top:300px;left:200px;',
'document_url' => route('pbg-task.index', ['menu_id' => $menus->where('url','pbg-task.index')->first()->id, 'filter' => 'verified'])
])
@endcomponent
@@ -137,7 +127,8 @@
'document_type' => 'Berkas',
'document_id' => 'chart-berkas-belum-terverifikasi',
'visible_small_circle' => true,
'style' => 'top:300px;left:600px;'
'style' => 'top:300px;left:600px;',
'document_url' => route('pbg-task.index', ['menu_id' => $menus->where('url','pbg-task.index')->first()->id, 'filter' => 'non-verified'])
])
@endcomponent
@@ -154,7 +145,8 @@
'document_type' => 'Berkas',
'document_id' => 'chart-realisasi-tebit-pbg',
'visible_small_circle' => true,
'style' => 'top:550px;left:100px;'
'style' => 'top:550px;left:100px;',
'document_url' => 'https://docs.google.com/spreadsheets/d/1QoXzuLdEX3MK70Yrfigz0Qj5rAt4T819jX85vubBNdY/edit?gid=1514195399#gid=1514195399'
])
@endcomponent
@@ -167,7 +159,8 @@
'document_type' => 'Berkas',
'document_id' => 'chart-menunggu-klik-dpmptsp',
'visible_small_circle' => true,
'style' => 'top:550px;left:400px'
'style' => 'top:550px;left:400px',
'document_url' => 'https://docs.google.com/spreadsheets/d/1QoXzuLdEX3MK70Yrfigz0Qj5rAt4T819jX85vubBNdY/edit?gid=1514195399#gid=1514195399'
])
@endcomponent
@@ -180,7 +173,8 @@
'document_type' => 'Berkas',
'document_id' => 'chart-proses-dinas-teknis',
'visible_small_circle' => true,
'style' => 'top:550px;left:700px'
'style' => 'top:550px;left:700px',
'document_url' => 'https://docs.google.com/spreadsheets/d/1QoXzuLdEX3MK70Yrfigz0Qj5rAt4T819jX85vubBNdY/edit?gid=1514195399#gid=1514195399'
])
@endcomponent
</div>

View File

@@ -21,11 +21,20 @@
<div class="wrapper">
<div id="lack-of-potential-fixed-container" class="" style="width:1400px;height:770px;position:relative;margin:auto;z-index:1;">
<div style="position: absolute; top: 200px; left: 50px;">
<x-custom-circle title="Restoran" size="small" style="background-color: #0e4753;" visible_data="true" data_id="restoran-count" data_count="0" />
<x-custom-circle title="Restoran" size="small" style="background-color: #0e4753;"
visible_data="true" data_id="restoran-count" data_count="0"
document_url="{{ route('web-spatial-plannings.index', ['menu_id' => $menus->where('url','web-spatial-plannings.index')->first()->id]) }}"
/>
<div class="square dia-top-left-bottom-right" style="top:30px;left:50px;width:150px;height:120px;"></div>
<x-custom-circle title="PBB Bangunan" visible_data="true" data_id="pbb-bangunan-count" data_count="0" size="small" style="background-color: #0e4753;" />
<x-custom-circle title="PBB Bangunan" visible_data="true" data_id="pbb-bangunan-count"
data_count="0" size="small" style="background-color: #0e4753;"
document_url="{{ route('web-spatial-plannings.index', ['menu_id' => $menus->where('url','web-spatial-plannings.index')->first()->id]) }}"
/>
<div class="square" style="width:150px;height:2px;background-color:black;left:50px;top:150px;"></div>
<x-custom-circle title="Reklame" visible_data="true" data_id="reklame-count" data_count="0" size="small" style="background-color: #0e4753;" />
<x-custom-circle title="Reklame" visible_data="true" data_id="reklame-count"
data_count="0" size="small" style="background-color: #0e4753;"
document_url="{{ route('web-advertisements.index', ['menu_id' => $menus->where('url','web-advertisements.index')->first()->id]) }}"
/>
<div class="square dia-top-right-bottom-left" style="top:140px;left:50px;width:150px;height:120px;"></div>
</div>
@@ -33,30 +42,39 @@
<div class="square dia-top-right-bottom-left" style="top:-100px;left:30px;width:150px;height:120px;"></div>
<div class="square dia-top-left-bottom-right" style="top:-100px;left:120px;width:120px;height:120px;"></div>
<x-custom-circle title="BAPENDA" size="small" style="float:left;background-color: #234f6c;" />
<x-custom-circle title="PDAM" visible_data="true" data_id="pdam-count" data_count="0" visible_data_type="true" data_type="Pelanggan" size="small" style="float:left;background-color: #234f6c;" />
<x-custom-circle title="KECAMATAN" size="small" style="float:left;background-color: #234f6c;" />
<x-custom-circle title="PDAM" visible_data="true" data_id="pdam-count"
visible_data_type="true" data_type="Pelanggan"
size="small" style="float:left;background-color: #234f6c;position: absolute;margin-left: 99px;"
document_url="{{ route('customers', ['menu_id' => $menus->where('url','customers')->first()->id]) }}"
/>
<x-custom-circle title="KECAMATAN" size="small" style="float:left;background-color: #234f6c;position: absolute;margin-left: 198px;" />
</div>
<div style="position: absolute; top: 0px; left: 270px;">
<div style="position: absolute; top: 0px; left: 270px;">
<div class="square" style="width:5px;height:600px;background-color:black;left:70px;top:50px;"></div>
<div class="square dia-top-left-bottom-right" style="top:350px;left:-50px;width:120px;height:120px;"></div>
<div class="square dia-top-right-bottom-left" style="top:350px;left:70px;width:120px;height:120px;"></div>
<x-custom-circle title="Rumah Tinggal" size="small" style="background-color: #234f6c;margin:auto;" />
<x-custom-circle title="Non Usaha" size="large" style="background-color: #3a968b;margin-top:20px;" />
<x-custom-circle title="USAHA" size="large" style="background-color: #627c8b;margin-top:150px;" />
<x-custom-circle title="USAHA" size="large" style="background-color: #627c8b;margin-top:150px;position: absolute;" />
</div>
<div style="position: absolute; top: 650px; left: 110px;">
<div style="position: absolute; top: 650px; left: 110px;">
<div class="square dia-top-right-bottom-left" style="top:-110px;left:40px;width:200px;height:120px;"></div>
<div class="square dia-top-right-bottom-left" style="top:-110px;left:90px;width:150px;height:170px;"></div>
<div class="square dia-top-left-bottom-right" style="top:-110px;left:230px;width:150px;height:170px;"></div>
<div class="square dia-top-left-bottom-right" style="top:-110px;left:260px;width:200px;height:180px;"></div>
<x-custom-circle title="Villa" size="small" style="float:left;background-color: #234f6c;" visible_data="true" data_id="villa-count" data_count="0" />
<x-custom-circle title="Villa" size="small" style="float:left;background-color: #234f6c;"
visible_data="true" data_id="villa-count" data_count="0"
document_url="{{ route('web-tourisms.index', ['menu_id' => $menus->where('url','web-tourisms.index')->first()->id]) }}"
/>
<x-custom-circle title="Pabrik" size="small" style="float:left;background-color: #234f6c;" />
<x-custom-circle title="Jalan Protocol" size="small" style="float:left;background-color: #234f6c;" />
<x-custom-circle title="Ruko" size="small" style="float:left;background-color: #234f6c;" />
<x-custom-circle title="Pariwisata" size="small" style="float:left;background-color: #234f6c; margin-right: 20px;" visible_data="true" data_id="pariwisata-count" data_count="0" />
<x-custom-circle title="Pariwisata" size="small" style="float:left;background-color: #234f6c; margin-right: 20px;"
visible_data="true" data_id="pariwisata-count" data_count="0"
document_url="{{ route('web-tourisms.index', ['menu_id' => $menus->where('url','web-tourisms.index')->first()->id]) }}"
/>
<div class="square" style="width:150px;height:2px;background-color:black;left:350px;top:50px;"></div>
<x-custom-circle title="DISBUDPAR" size="small" style="background-color: #3a968b;" />
</div>
@@ -75,7 +93,10 @@
'style' => 'margin-left:180px;top:-20px;'
])
@endcomponent
<x-custom-circle title="Tata Ruang" size="large" style="background-color: #da6635;float:left;margin-left:250px;" visible_data="true" data_id="tata-ruang-count" data_count="0" />
<x-custom-circle title="Tata Ruang" size="large" style="background-color: #da6635;float:left;margin-left:250px;"
visible_data="true" data_id="tata-ruang-count" data_count="0"
document_url="{{ route('web-spatial-plannings.index', ['menu_id' => $menus->where('url','web-spatial-plannings.index')->first()->id]) }}"
/>
</div>
<div style="position: absolute; top: 310px; left: 1150px;">
@@ -102,7 +123,10 @@
<div style="position: absolute; top: 50px; left: 1100px;">
<x-custom-circle title="Non Usaha" size="large" style="background-color: #3a968b;margin-top:20px;" />
<x-custom-circle title="USAHA" size="large" style="background-color: #627c8b;margin-top:260px;" visible_data="true" data_id="tata-ruang-usaha-count" data_count="0" />
<x-custom-circle title="USAHA" size="large" style="background-color: #627c8b;margin-top:260px;"
visible_data="true" data_id="tata-ruang-usaha-count" data_count="0"
document_url="{!! route('pbg-task.index', ['filter' => 'business', 'menu_id' => $menus->where('url','pbg-task.index')->first()->id]) !!}"
/>
</div>
</div>
</div>

View File

@@ -27,7 +27,8 @@
'document_type' => 'Berkas',
'document_id' => 'outside-system-non-business',
'visible_small_circle' => true,
'style' => 'top:10px;'
'style' => 'top:10px;',
'document_url' => route('pbg-task.index', ['menu_id' => 13, 'filter' => 'non-business'])
])
@endcomponent
<div class="square dia-top-right-bottom-left" style="top:10px;left:180px;width:230px;height:120px;"></div>
@@ -37,7 +38,8 @@
'document_type' => 'Berkas',
'document_id' => 'outside-system-business',
'visible_small_circle' => true,
'style' => 'top:300px;'
'style' => 'top:300px;',
'document_url' => route('pbg-task.index', ['menu_id' => 13, 'filter' => 'business'])
])
@endcomponent
<div class="square dia-top-right-bottom-left" style="top:320px;left:170px;width:200px;height:100px;"></div>

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')
@@ -22,9 +42,29 @@
<a href="{{ route('pbg-task.create')}}" class="btn btn-success btn-sm d-block d-sm-inline w-auto">Create</a>
@endif
</div>
<form id="filter-form">
<div class="row pb-3">
<div class="col-md-4">
<select name="filter" id="filter-select" class="form-select">
@foreach ($filterOptions as $key => $label)
<option value="{{ $key }}" {{ request('filter') == $key ? 'selected' : '' }}>
{{ $label }}
</option>
@endforeach
</select>
<input name="menu_id" value="13" type="hidden" />
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">Apply</button>
</div>
</div>
</form>
<!-- Table or Data Display Area -->
<div id="table-pbg-tasks"
data-updater="{{ $updater }}"
data-destroyer="{{ $destroyer }}">
data-updater="{{ $updater }}"
data-destroyer="{{ $destroyer }}">
</div>
</div>
</div>
@@ -71,7 +111,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 +120,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 +148,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

@@ -0,0 +1,229 @@
@extends('layouts.base', ['subtitle' => 'Quick Search'])
@section('css')
@vite(['resources/scss/pages/quick-search/detail.scss'])
@vite(['node_modules/gridjs/dist/theme/mermaid.min.css'])
@endsection
@section('content')
<div class="container qs-detail-container pt-3">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Detail Informasi Permohonan PBG</h5>
<a href="javascript:history.back()" class="btn btn-primary">Back</a>
</div>
<div class="card-body">
<div class="row gy-3 gx-4">
<div class="col-md-6">
<dl class="row mb-0">
<dt class="col-sm-5">Nama Pemohon</dt>
<dd class="col-sm-7">{{ $data->name ?? '-' }}</dd>
<dt class="col-sm-5">Nama Pemilik</dt>
<dd class="col-sm-7">{{ $data->owner_name ?? '-' }}</dd>
<dt class="col-sm-5">Jenis Permohonan</dt>
<dd class="col-sm-7">{{ isset($data->application_type) ? $applicationTypes[$data->application_type] : '-' }}</dd>
<dt class="col-sm-5">Kondisi</dt>
<dd class="col-sm-7">{{ $data->condition ?? '-' }}</dd>
<dt class="col-sm-5">Nomor Registrasi</dt>
<dd class="col-sm-7">{{ $data->registration_number ?? '-'}}</dd>
<dt class="col-sm-5">Nomor Dokumen</dt>
<dd class="col-sm-7">{{ $data->document_number ?? '-' }}</dd>
<dt class="col-sm-5">Status</dt>
<dd class="col-sm-7">{{ isset($data->status) ? $statusOptions[$data->status] : '-' }}</dd>
</dl>
</div>
<div class="col-md-6">
<dl class="row mb-0">
<dt class="col-sm-5">Alamat</dt>
<dd class="col-sm-7">{{ $data->address ?? '-' }}</dd>
<dt class="col-sm-5">Status SLF</dt>
<dd class="col-sm-7">{{ $data->slf_status_name ?? '-' }}</dd>
<dt class="col-sm-5">Fungsi Bangunan</dt>
<dd class="col-sm-7">{{ $data->function_type ?? '-' }}</dd>
<dt class="col-sm-5">Jenis Konsultasi</dt>
<dd class="col-sm-7">{{ $data->consultation_type ?? '-' }}</dd>
<dt class="col-sm-5">Jatuh Tempo</dt>
<dd class="col-sm-7">{{$data->due_date ? \Carbon\Carbon::parse($data->due_date)->format('d M Y') : '-' }}</dd>
<dt class="col-sm-5">Tanggal Dibuat</dt>
<dd class="col-sm-7">{{ \Carbon\Carbon::parse($data->task_created_at)->format('d M Y H:i') }}</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card">
<div class="card-header">
<ul class="nav nav-tabs nav-justified">
<li class="nav-item">
<a href="#pbgTaskRetributions" data-bs-toggle="tab" aria-expanded="false"
class="nav-link active">
<span class="d-sm-block">PBG Task Retributions</span>
</a>
</li>
<li class="nav-item">
<a href="#pbgTaskIntegration" data-bs-toggle="tab" aria-expanded="false" class="nav-link">
<span class="d-sm-block">PBG Task Index Integrations</span>
</a>
</li>
<li class="nav-item">
<a href="#pbgTaskPrasarana" data-bs-toggle="tab" aria-expanded="false" class="nav-link">
<span class="d-sm-block">PBG Task Prasarana</span>
</a>
</li>
<li class="nav-item">
<a href="#pbgTaskAssignments" data-bs-toggle="tab" aria-expanded="false" class="nav-link">
<span class="d-sm-block">Penugasan</span>
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane active" id="pbgTaskRetributions">
@if ($data->pbg_task_retributions)
<div class="row">
<div class="col-md-6">
<dl class="row mb-0">
<dt class="col-sm-4">Luas Bangunan</dt>
<dd class="col-sm-8">{{$data->pbg_task_retributions->luas_bangunan ?? '-'}}</dd>
<dt class="col-sm-4">Indeks Lokalitas</dt>
<dd class="col-sm-8">{{$data->pbg_task_retributions->indeks_lokalitas ?? '-'}}</dd>
<dt class="col-sm-4">Wilayah SHST</dt>
<dd class="col-sm-8">{{$data->pbg_task_retributions->wilayah_shst ?? '-'}}</dd>
<dt class="col-sm-4">Nama Kegiatan</dt>
<dd class="col-sm-8">{{$data->pbg_task_retributions->kegiatan_name ?? '-'}}</dd>
<dt class="col-sm-4">Nilai SHST</dt>
<dd class="col-sm-8">{{$data->pbg_task_retributions->nilai_shst ?? '-'}}</dd>
<dt class="col-sm-4">Indeks Integrasi</dt>
<dd class="col-sm-8">{{$data->pbg_task_retributions->indeks_terintegrasi ?? '-'}}</dd>
<dt class="col-sm-4">Indeks Bg Terbangun</dt>
<dd class="col-sm-8">{{$data->pbg_task_retributions->indeks_bg_terbangun ?? '-'}}</dd>
</dl>
</div>
<div class="col-md-6">
<dl class="row mb-0">
<dt class="col-sm-4">Nilai Retribusi Bangunan</dt>
<dd class="col-sm-8">{{$data->pbg_task_retributions->nilai_retribusi_bangunan ?? '-'}}</dd>
<dt class="col-sm-4">Nilai Prasarana</dt>
<dd class="col-sm-8">{{$data->pbg_task_retributions->nilai_prasarana ?? '-'}}</dd>
<dt class="col-sm-4">PBG Dokumen</dt>
<dd class="col-sm-8">{{$data->pbg_task_retributions->pbg_document ?? '-'}}</dd>
<dt class="col-sm-4">Underpayment</dt>
<dd class="col-sm-8">{{$data->pbg_task_retributions->underpayment ?? '-'}}</dd>
<dt class="col-sm-4">SKRD Amount</dt>
<dd class="col-sm-8">{{$data->pbg_task_retributions->skrd_amount ?? '-'}}</dd>
</dl>
</div>
</div>
@else
<div class="alert alert-secondary" role="alert">
Data Not Available
</div>
@endif
</div>
<div class="tab-pane" id="pbgTaskIntegration">
@if ($data->pbg_task_index_integrations)
<dl class="row">
<dt class="col-sm-4">Indeks Fungsi Bangunan</dt>
<dd class="col-sm-8">{{$data->pbg_task_index_integrations->indeks_fungsi_bangunan ?? '-'}}</dd>
<dt class="col-sm-4">Indeks Parameter Kompleksitas</dt>
<dd class="col-sm-8">{{$data->pbg_task_index_integrations->indeks_parameter_kompleksitas ?? '-'}}</dd>
<dt class="col-sm-4">Indeks Parameter Permanensi</dt>
<dd class="col-sm-8">{{$data->pbg_task_index_integrations->indeks_parameter_permanensi ?? '-'}}</dd>
<dt class="col-sm-4">Indeks Parameter Ketinggian</dt>
<dd class="col-sm-8">{{$data->pbg_task_index_integrations->indeks_parameter_ketinggian ?? '-'}}</dd>
<dt class="col-sm-4">Faktor Kepemilikan</dt>
<dd class="col-sm-8">{{$data->pbg_task_index_integrations->faktor_kepemilikan ?? '-'}}</dd>
<dt class="col-sm-4">Indeks Terintegrasi</dt>
<dd class="col-sm-8">{{$data->pbg_task_index_integrations->indeks_terintegrasi ?? '-'}}</dd>
<dt class="col-sm-4">Total</dt>
<dd class="col-sm-8">{{$data->pbg_task_index_integrations->total ?? '-'}}</dd>
</dl>
@else
<div class="alert alert-secondary" role="alert">
Data Not Available
</div>
@endif
</div>
<div class="tab-pane" id="pbgTaskPrasarana">
<div class="row d-flex flex-warp gap-3 justify-content-center">
@if ($data->pbg_task_retributions && $data->pbg_task_retributions->pbg_task_prasarana)
@foreach ($data->pbg_task_retributions->pbg_task_prasarana as $prasarana)
<div class="border p-3 rounded shadow-sm col-md-4">
<dl class="row">
<dt class="col-sm-4">Prasarana Type</dt>
<dd class="col-sm-8">{{$prasarana->prasarana_type ?? '-'}}</dd>
<dt class="col-sm-4">Building Type</dt>
<dd class="col-sm-8">{{$prasarana->building_type ?? '-'}}</dd>
<dt class="col-sm-4">Total</dt>
<dd class="col-sm-8">{{$prasarana->total ?? '-'}}</dd>
<dt class="col-sm-4">Quantity</dt>
<dd class="col-sm-8">{{$prasarana->quantity ?? '-'}}</dd>
<dt class="col-sm-4">Unit</dt>
<dd class="col-sm-8">{{$prasarana->unit ?? '-'}}</dd>
<dt class="col-sm-4">Index Prasarana</dt>
<dd class="col-sm-8">{{$prasarana->index_prasarana ?? '-'}}</dd>
<dt class="col-sm-4">Created At</dt>
<dd class="col-sm-8">{{$prasarana->created_at ? \Carbon\Carbon::parse($prasarana->created_at)->format('d M Y') : '-'}}</dd>
</dl>
</div>
@endforeach
@else
<div class="alert alert-secondary" role="alert">
Data Not Available
</div>
@endif
</div>
</div>
<div class="tab-pane" id="pbgTaskAssignments">
<input type="hidden" id="url_task_assignments" value="{{ route('api.quick-search-task-assignments', ['uuid' => $data->uuid]) }}" />
<div id="table-pbg-task-assignments"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('scripts')
@vite(['resources/js/quick-search/detail.js'])
@endsection

View File

@@ -0,0 +1,35 @@
@extends('layouts.base', ['subtitle' => 'Quick Search'])
@section('css')
@vite(['resources/scss/pages/quick-search/index.scss'])
@endsection
@section('body-attribuet')
class="gsp-body"
@endsection
@section('content')
<div class="position-absolute top-0 end-0 p-3">
<a href="{{ route('login') }}" class="btn btn-md btn-secondary">
Login
</a>
</div>
<div class="container min-vh-100 d-flex justify-content-center align-items-center gsp-body">
<div class="w-100" style="max-width: 700px;">
<div class="text-center mb-4">
<img src="{{ asset('images/simbg-dputr.png') }}" alt="PBG Icon" class="img-fluid gsp-icon mb-3">
<h1 class="gsp-title">SIBEDAS PBG</h1>
</div>
<div class="d-flex flex-column flex-sm-row align-items-stretch gap-2">
<div class="flex-fill">
<input type="text" class="gsp-input" id="searchInput" placeholder="Cari..." autocomplete="off" />
</div>
<button class="gsp-btn" id="searchBtn">Cari</button>
</div>
</div>
</div>
@endsection
@section('scripts')
@vite(['resources/js/quick-search/index.js'])
@endsection

View File

@@ -0,0 +1,44 @@
@extends('layouts.base', ['subtitle' => 'Quick Search'])
@section('css')
@vite(['resources/scss/pages/quick-search/result.scss'])
@vite(['node_modules/gridjs/dist/theme/mermaid.min.css'])
@endsection
@section('content')
<input type="hidden" value="{{ route('quick-search-datatable', ['search' => $keyword]) }}" id="base_url_datatable" />
<div class="container qs-wrapper">
<div class="qs-toolbar d-flex justify-content-between align-items-center pt-4 pb-4">
<!-- Back Button -->
<a href="{{ route('search') }}" class="btn btn-light border me-3">
Kembali
</a>
<!-- Search Area (no form action) -->
<div class="qs-search-form d-flex align-items-center">
<input
type="text"
id="search_input"
class="gsp-input me-2"
value="{{ $keyword }}"
placeholder="Cari data..."
required
/>
<button type="button" id="search_button" class="gsp-btn">Cari</button>
</div>
</div>
<div class="qs-header mb-3">
<h2>Hasil Pencarian: <em>{{ $keyword }}</em></h2>
<p>Berikut adalah data hasil pencarian berdasarkan kata kunci yang Anda masukkan.</p>
</div>
<div class="qs-table-wrapper">
<div class="p-3" id="datatable-quick-search-result"></div>
</div>
</div>
@endsection
@section('scripts')
@vite(['resources/js/quick-search/result.js'])
@endsection

View File

@@ -0,0 +1,21 @@
@extends('layouts.vertical', ['subtitle' => 'Laporan Pertumbuhan'])
@section('css')
@endsection
@section('content')
@include('layouts.partials/page-title', ['title' => 'Laporan', 'subtitle' => 'Laporan Pertumbuhan'])
<div class="card">
<div class="card-body">
<div class="row">
<div id="chart-growth-report" data-url="{{ route('api.growth') }}"></div>
</div>
</div>
</div>
@endsection
@section('scripts')
@vite(['resources/js/report/growth-report/index.js'])
@endsection

View File

@@ -7,9 +7,11 @@ use App\Http\Controllers\Api\DashboardController;
use App\Http\Controllers\Api\DataSettingController;
use App\Http\Controllers\Api\GlobalSettingsController;
use App\Http\Controllers\Api\GoogleSheetController;
use App\Http\Controllers\Api\GrowthReportAPIController;
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 +74,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 +111,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 +183,13 @@ 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');
});
Route::controller(GrowthReportAPIController::class)->group(function(){
Route::get('/growth','index')->name('api.growth');
});
});

View File

@@ -14,6 +14,9 @@ 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\QuickSearchController;
use App\Http\Controllers\Report\GrowthReportsController;
use App\Http\Controllers\ReportPaymentRecapsController;
use App\Http\Controllers\ReportPbgPTSPController;
use App\Http\Controllers\RequestAssignment\PbgTaskController;
@@ -33,6 +36,12 @@ use Illuminate\Support\Facades\Route;
require __DIR__ . '/auth.php';
Route::get('/search', [QuickSearchController::class, 'index'])->name('search');
Route::get('/search-result', [QuickSearchController::class, 'search_result'])->name('search-result');
Route::get('/quick-search-datatable', [QuickSearchController::class, 'quick_search_datatable'])->name('quick-search-datatable');
Route::get('/quick-search/{id}', [QuickSearchController::class, 'show'])->name('quick-search.detail');
Route::get('/quick-search/{uuid}/task-assignments', [QuickSearchController::class, 'task_assignments'])->name('api.quick-search-task-assignments');
// auth
Route::group(['middleware' => 'auth'], function(){
@@ -64,6 +73,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);
@@ -154,6 +164,10 @@ Route::group(['middleware' => 'auth'], function(){
Route::controller(ReportPbgPTSPController::class)->group(function (){
Route::get('/report-pbg-ptsp', 'index')->name('report-pbg-ptsp');
});
Route::controller(GrowthReportsController::class)->group(function (){
Route::get('/growths','index')->name('growths');
});
});
// approval

File diff suppressed because one or more lines are too long

View File

@@ -20,6 +20,9 @@ export default defineConfig({
"resources/scss/components/_custom_circle.scss",
"resources/scss/dashboards/potentials/_inside_system.scss",
"resources/scss/dashboards/potentials/_outside_system.scss",
"resources/scss/pages/quick-search/detail.scss",
"resources/scss/pages/quick-search/index.scss",
"resources/scss/pages/quick-search/result.scss",
"node_modules/quill/dist/quill.snow.css",
"node_modules/quill/dist/quill.bubble.css",
@@ -109,6 +112,12 @@ export default defineConfig({
"resources/js/pbg-task/create.js",
// google-sheets
"resources/js/data/google-sheet/index.js",
// quick-search
"resources/js/quick-search/index.js",
"resources/js/quick-search/result.js",
"resources/js/quick-search/detail.js",
// growth-report
"resources/js/report/growth-report/index.js",
// dummy
"resources/js/approval/index.js",
"resources/js/invitations/index.js",