feat(rekap-monitoring-so) : tambah rekap monitoring so

This commit is contained in:
majid
2025-07-03 09:51:57 +07:00
parent e72e82ea55
commit ae15e1983f
4 changed files with 571 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
<?php
namespace Modules\Lpj\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Modules\Lpj\Services\LaporanMonitoringSoService;
class LaporanMonitoringSoController extends Controller
{
private $laporanMonitoringSoService;
public function __construct(LaporanMonitoringSoService $laporanMonitoringSoService)
{
$this->laporanMonitoringSoService = $laporanMonitoringSoService;
}
/**
* Display a listing of the resource.
*/
public function index()
{
$user = auth()->user()->load('roles');
$result = $this->laporanMonitoringSoService->progresPengerjaanLaporan($user);
return view('lpj::laporan-monitoring.index', compact('result'));
}
/**
* Show details data.
* @return Response
*/
public function show($id){
return view('lpj::laporan-monitoring.show', compact('id'));
}
public function dataForDatatablePenilai(Request $request, $id){
return $this->laporanMonitoringSoService->showDetailsPermohonan($request, $id);
}
}

View File

@@ -0,0 +1,228 @@
<?php
namespace Modules\Lpj\Services;
use Modules\Lpj\Models\TeamsUsers;
use Modules\Lpj\Models\Teams;
use Modules\Lpj\Models\Debiture;
use Modules\Lpj\Models\Permohonan;
use Modules\Lpj\Models\Penilaian;
use Modules\Lpj\Models\StatusPermohonan;
use Carbon\Carbon;
class LaporanMonitoringSoService
{
public function progresPengerjaanLaporan($user){
// Inisialisasi regionId dan teamId
$regionId = $teamId = null;
if ($user->roles->pluck('name')->contains('senior-officer')) {
$userTeam = TeamsUsers::with('team')->firstWhere('user_id', $user->id);
$regionId = $userTeam?->team->regions_id;
$teamId = $userTeam?->teams_id;
}
$teamsActivity = TeamsUsers::with(['user', 'team', 'team.regions', 'user.roles'])
->whereHas('team', function ($q) use ($regionId, $teamId) {
$q->when($regionId, fn ($q) => $q->where('regions_id', $regionId))
->when($teamId, fn ($q) => $q->where('id', $teamId));
})
->where('user_id', '!=', $user->id)
->whereHas('user.roles', fn ($q) => $q->whereIn('name', ['surveyor', 'surveyor-penilai']))
->get();
$teamId = is_array($teamId) ? $teamId : [$teamId];
$teamPenilai = Teams::with(['regions', 'teamsUsers', 'teamsUsers.user'])->whereNotIn(
'id',
$teamId
)->get();
return [
'teamsActivity' => $teamsActivity,
'teamPenilai' => $teamPenilai,
];
}
public function showDetailsPermohonan($request,$id)
{
$query = Penilaian::with([
'permohonan',
'permohonan.debiture',
'permohonan.tujuanPenilaian',
'permohonan.debiture.documents.jenisJaminan',
'userPenilai' => function ($query) use ($id) {
$query->where('user_id', $id);
},
'permohonan.penilai',
'permohonan.approveEo',
'permohonan.approveDd',
'permohonan.approveSo',
])
->whereHas('userPenilai', function ($q) use ($id) {
$q->where('user_id', $id);
})
->whereHas('permohonan', function ($q) {
$q->whereIn('status', [
'assign',
'survey-completed',
'proses-laporan',
'paparan',
'proses-paparan',
'revisi-laporan',
'revisi-paparan',
'survey',
'proses-survey',
'request-reschedule',
'reschedule',
'rejected-reschedule',
'approved-reschedule',
'revisi-survey',
'revisi-pembayaran'
]);
});
// Filter pencarian
if ($request->has('search') && !empty($request->get('search'))) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
$q->where('nomor_registrasi', 'LIKE', "%$search%")
->orWhere('status', 'LIKE', "%$search%");
});
}
// Sorting
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
$order = $request->get('sortOrder');
$column = $request->get('sortField');
$query->orderBy($column, $order);
}
// Hitung total records
$totalRecords = $query->count();
// Pagination
$size = $request->get('size', 10);
$page = $request->get('page', 1);
$offset = ($page - 1) * $size;
// Ambil data dengan pagination
$data = $query->skip($offset)->take($size)->get();
$data = $data->map(function ($item) {
$jeniAsset = null;
$statusPembayaran = trim(strtolower($item->permohonan->status_bayar ?? ''));
$tujuanPenilaian = $item->permohonan->tujuanPenilaian->name ?? null;
$plafond = $item->permohonan->nilaiPlafond->name ?? null;
$now = Carbon::now();
$type_report = $item->permohonan->penilai->type ?? "";
$hari = $hariPaparan = 0;
if ($type_report == "sederhana") {
$hari = 2;
$item->paparan = 'Tidak Ada';
} else {
if ($plafond == '< 2M') {
$item->paparan = 'Tidak Ada';
$hari = 3;
} elseif ($plafond == '2 M - 5 M') {
$hari = 3;
$hariPaparan = 2;
} else {
$hari = 5;
$hariPaparan = 3;
}
}
if ($tujuanPenilaian == 'RAP') {
$hari = 2;
$hariPaparan = 2;
}
if ($item->permohonan && $item->permohonan->debiture) {
$jeniAsset = $item->permohonan->debiture->documents->first() ?? null;
}
/*$hariTambahan = 0;
if ($tujuanPenilaian == 'RAP') {
$hariTambahan = 2;
} else {
if ($statusPembayaran == 'sudah_bayar') {
$hariTambahan = 1; // H+1 untuk yang sudah bayar
} else {
$hariTambahan = 2; // H+2 untuk yang belum bayar
}
}*/
$tanggalMulai = $item->waktu_penilaian;
if ($tanggalMulai) {
if (!$tanggalMulai instanceof Carbon) {
$tanggalMulai = Carbon::parse($tanggalMulai);
}
$hariKerjaBerikutnya = hitungHariKerja($tanggalMulai->toDateString(), $tanggalMulai->copy()->addDays(1));
$hariKerjaBerikutnya = max($hariKerjaBerikutnya, 1);
$tanggalMulai = $tanggalMulai->copy()->addDays($hariKerjaBerikutnya);
// Konversi string tanggal ke objek Carbon jika belum
if (!$tanggalMulai instanceof Carbon) {
$tanggalMulai = Carbon::parse($tanggalMulai);
}
// Hitung tanggal selesai berdasarkan hari tambahan
$tanggalSelesai = $tanggalMulai->copy()->addDays($hari);
$tanggalPaparan = $tanggalMulai->copy()->addDays($hariPaparan);
// Hitung hari kerja
$hariKerja = hitungHariKerja($tanggalMulai->toDateString(), $tanggalSelesai->toDateString());
$hariKerja = max($hariKerja, $hari);
$hariKerjaPaparan = hitungHariKerja($tanggalMulai->toDateString(), $tanggalPaparan->toDateString());
$hariKerjaPaparan = max($hariKerjaPaparan, $hariPaparan);
// Set due date SLA
$dueDateSla = $tanggalMulai->copy()->addDays($hariKerja);
$dueDateSlaPaparan = $tanggalMulai->copy()->addDays($hariKerjaPaparan);
// Cek apakah sudah melewati due date
/*if ($now->greaterThan($dueDateSla)) {
$item->due_date_sla = null;
} else {
$item->due_date_sla = $dueDateSla->toDateString();
}*/
$item->due_date_sla = $dueDateSla->toDateString();
$item->paparan = $dueDateSlaPaparan->toDateString();
} else {
$item->due_date_sla = null;
$item->paparan = null;
}
return $item;
});
$filteredRecords = $data->count();
$pageCount = ceil($totalRecords / $size);
// Return data dalam bentuk JSON
return response()->json([
'draw' => $request->get('draw'),
'recordsTotal' => $totalRecords,
'recordsFiltered' => $filteredRecords,
'pageCount' => $pageCount,
'page' => $page,
'totalCount' => $totalRecords,
'data' => $data
]);
}
}

View File

@@ -0,0 +1,89 @@
@extends('layouts.main')
@section('breadcrumbs')
{{-- {{ Breadcrumbs::render('laporan-hasil-penilaian-jaminan-internal-external') }} --}}
@endsection
@section('content')
<div class="w-full grid gap-5 lg:gap-7.5 mx-auto">
<!-- Filter Card -->
<!-- Data Table Card -->
<div class="card border border-agi-100 card-grid min-w-full" data-datatable="false" data-datatable-page-size="10"
data-datatable-state-save="false" id="laporan-hasil-penilaian-jaminan-internal-external-table"
data-api-url="{{ route('laporan-hasil-penilaian-jaminan-internal-external.data') }}">
<div class="card-header bg-agi-50 py-5 flex-wrap">
<h3 class="card-title">
Rekap Progres Pengerjaan Laporan Penilai
</h3>
</div>
<div class="card-body">
<div class="scrollable-x-auto">
<table class="table table-auto table-border align-middle text-gray-700 font-medium text-sm"
data-datatable-table="true">
<thead>
<tr>
<th class="w-14">
<input class="checkbox checkbox-sm" data-datatable-check="true" type="checkbox" />
</th>
<th class="min-w-[150px]" data-datatable-column="nama_penilai">
<span class="sort"> <span class="sort-label"> Nama Penilai </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="total_task">
<span class="sort"> <span class="sort-label"> Total Task </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="actions">
<span class="sort"> <span class="sort-label"> Aksi </span>
<span class="sort-icon"> </span> </span>
</th>
</tr>
</thead>
@php
$sortedTeamsActivity = $result['teamsActivity']->sortBy(function ($item) {
return $item->team->regions->penilaiTeam
->filter(function ($penilaiTeam) {
$permohonan = optional($penilaiTeam->penilaian)->permohonan;
return $permohonan !== null;
})
->count();
});
@endphp
<tbody>
@foreach ($sortedTeamsActivity as $teamActivity)
@php
$totalTask = countPermohonanForUser($teamActivity->user->id);
@endphp
<tr>
<td class="w-14">
<input class="checkbox checkbox-sm" data-datatable-check="true" type="checkbox" />
</td>
<td class="min-w-[150px]">
{{ $teamActivity->user->name }}
</td>
<td class="min-w-[150px]">
{{ $totalTask }}
</td>
<td class="min-w-[150px]">
<a href="{{ route('laporan-monitoring.show', $teamActivity->user->id) }}"
class="btn btn-sm btn-primary">
<i class="ki-outline ki-eye"></i>
Lihat
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,211 @@
@extends('layouts.main')
@section('breadcrumbs')
{{-- {{ Breadcrumbs::render('laporan-hasil-penilaian-jaminan-internal-external') }} --}}
@endsection
@section('content')
<div class="grid">
<div class="card border border-agi-100 card-grid min-w-full" data-datatable="false" data-datatable-page-size="10"
data-datatable-state-save="false" id="laporan-penilai-table"
data-api-url="{{ route('laporan-monitoring.datatables', ['id' => $id]) }}">
<div class="card-header bg-agi-50 py-5 flex-wrap">
<h3 class="card-title">
Daftar Penilaian {{ $nama_penilai ?? '' }}
</h3>
<div class="flex flex-wrap gap-2 lg:gap-5">
<div class="flex flex-wrap gap-2.5">
<div class="h-[24px] border border-r-gray-200"></div>
<a class="btn btn-sm btn-light" href=""> Export to Excel </a>
<a class="btn btn-sm btn-info" href="{{ route('laporan-monitoring.index') }}">
<i class="ki-duotone ki-arrow-left fs-2"></i>
Back</a>
</div>
</div>
</div>
<div class="card-body">
<div class="scrollable-x-auto">
<table class="table table-auto align-middle text-gray-700 font-medium text-sm mb-4"
data-datatable-table="true">
<thead>
<tr>
<th class="min-w-[100px]">Nama Debitur</th>
<th class="min-w-[100px]">Tujuan Penilaian</th>
<th class="min-w-[100px]">Status Bayar</th>
<th class="min-w-[100px]">Jenis Asset</th>
<th class="min-w-[100px]">Penugasan</th>
<th class="min-w-[100px]">Jenis Report</th>
<th class="min-w-[100px]">Tgl Register</th>
<th class="min-w-[100px]">Tgl Assign</th>
<th class="min-w-[100px]">Tgl Kunjungan</th>
<th class="min-w-[100px]">Progress</th>
<th class="min-w-[100px]">SLA Laporan</th>
<th class="min-w-[100px]">SLA Paparan</th>
<th class="min-w-[100px]">Approve</th>
<th class="min-w-[50px] text-center">Keterangan</th>
<th class="min-w-[50px] text-center">Action</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div
class="card-footer justify-center md:justify-between flex-col md:flex-row gap-3 text-gray-600 text-2sm font-medium">
<div class="flex items-center gap-2">
Show
<select class="select select-sm w-16" data-datatable-size="true" name="perpage"></select> per page
</div>
<div class="flex items-center gap-4">
<span data-datatable-info="true"></span>
<div class="pagination" data-datatable-pagination="true"></div>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script type="module">
const element = document.querySelector('#laporan-penilai-table');
const apiUrl = element.getAttribute('data-api-url');
const dataTableOptions = {
apiEndpoint: apiUrl,
pageSize: 5,
order: [{
column: 'nomor_registrasi',
dir: 'asc'
}],
columns: {
nama_debitur: {
title: 'Nama Debitur',
render: (item, data) => {
return `${data.permohonan.debiture?.name}`
},
},
tujuan_penilaian: {
title: 'Tujuan Penilaian',
render: (item, data) => {
return `${data.permohonan.tujuan_penilaian?.name || ''}`;
},
},
status_bayar: {
title: 'Status Bayar',
render: (item, data) => {
const status = data.permohonan.status_bayar.replace(
/_/g,
' ');
const statusClass = data.permohonan.status_bayar ===
'belum_bayar' ? 'text-red-600' :
'text-green-600';
return `<span class="badge badge-sm badge-default font-bold ${statusClass} uppercase">
${status}
</span>`;
},
},
jenis_asset: {
title: 'Jenis Asset',
render: (item, data) =>
`${data.permohonan.debiture?.documents?.map(d => d.jenis_jaminan.name) || ''}`,
},
penugasan: {
title: 'Penugasan',
render: (item, data) => {
const roles = data.user_penilai?.map(d => d.role).join(
'<br>') || '-';
return roles;
},
},
jenis_report: {
title: 'Jenis Report',
render: (item, data) => {
return data.permohonan.penilai?.type_penilai || '-';
},
},
register: {
title: 'Register',
render: (item, data) =>
`${window.formatTanggalIndonesia(data.permohonan.created_at) || ''}`,
},
assign: {
title: 'Assign',
render: (item, data) =>
`${window.formatTanggalIndonesia(data.created_at)}`,
},
tanggal_kunjungan: {
title: 'Tgl Kunjungan',
render: (item, data) =>
`${window.formatTanggalIndonesia(data.waktu_penilaian) || ''}`,
},
progress: {
title: 'Progress',
render: (item, data) => {
return `<span class="badge badge-sm badge-default uppercase flex justify-center ">${data.permohonan.status}</span>`;
}
},
// tanggal kunjungan h+2 jika plafon di
due_date: {
title: 'Due Date',
render: (item, data) => {
if (!data.due_date_sla) {
return ``;
}
return `${window.formatTanggalIndonesia(data.due_date_sla)}`;
}
},
due_date: {
title: 'Due Date SLA',
render: (item, data) => {
if (!data.due_date_sla) {
return `-`;
}
return `${window.formatTanggalIndonesia(data.due_date_sla)}`;
},
},
paparan: {
title: 'Paparan',
render: (item, data) => {
if (!data.due_date_sla) {
return `-`;
}
return `${window.formatTanggalIndonesia(data.paparan)}`;
}
},
approve: {
title: 'Approve',
render: (item, data) => {
// Gabungkan nama dengan <br> untuk pemisah baris baru
let dataHtml = `
${data.permohonan?.approve_so?.name || ''}
<br>
${data.permohonan?.approve_eo?.name || ''}
<br>
${data.permohonan?.approve_dd?.name || ''}
`;
return dataHtml;
},
},
keterangan: {
title: 'Keterangan',
render: (item, data) => `${data.keterangan || ''}`,
}
},
};
let dataTable = new KTDataTable(element, dataTableOptions);
</script>
@endpush