Penambahan thema pada Menu Laporan per pintu pos
This commit is contained in:
BIN
app/.DS_Store
vendored
Normal file
BIN
app/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -89,13 +89,19 @@ public function run()
|
|||||||
|
|
||||||
private function checkPgcrypto()
|
private function checkPgcrypto()
|
||||||
{
|
{
|
||||||
$result = DB::select("
|
// $result = DB::select("
|
||||||
SELECT 1
|
// SELECT 1
|
||||||
FROM pg_extension
|
// FROM pg_extension
|
||||||
WHERE extname = 'pgcrypto'
|
// WHERE extname = 'pgcrypto'
|
||||||
");
|
// ");
|
||||||
|
|
||||||
return !empty($result);
|
// return !empty($result);
|
||||||
|
try {
|
||||||
|
DB::select("SELECT digest('test','sha1')");
|
||||||
|
return true;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function checkPegawaiColumns()
|
private function checkPegawaiColumns()
|
||||||
|
|||||||
69
app/Services/BcaServices.php
Normal file
69
app/Services/BcaServices.php
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
|
class BcaService
|
||||||
|
{
|
||||||
|
protected $apiKey;
|
||||||
|
protected $apiSecret;
|
||||||
|
protected $baseUrl = 'https://sandbox.bca.co.id';
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->apiKey = env('BCA_API_KEY');
|
||||||
|
$this->apiSecret = env('BCA_API_SECRET');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAccessToken()
|
||||||
|
{
|
||||||
|
$response = Http::asForm()
|
||||||
|
->withBasicAuth($this->apiKey, $this->apiSecret)
|
||||||
|
->post($this->baseUrl . '/api/oauth/token', [
|
||||||
|
'grant_type' => 'client_credentials'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $response->json()['access_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateSignature($method, $relativeUrl, $token, $body, $timestamp)
|
||||||
|
{
|
||||||
|
$bodyHash = hash('sha256', $body);
|
||||||
|
|
||||||
|
$stringToSign = strtoupper($method) . ':' .
|
||||||
|
$relativeUrl . ':' .
|
||||||
|
$token . ':' .
|
||||||
|
strtolower($bodyHash) . ':' .
|
||||||
|
$timestamp;
|
||||||
|
|
||||||
|
return hash_hmac('sha256', $stringToSign, $this->apiSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAccount($corporateId, $accountNumber)
|
||||||
|
{
|
||||||
|
$token = $this->getAccessToken();
|
||||||
|
|
||||||
|
$method = 'GET';
|
||||||
|
$relativeUrl = "/banking/v3/corporates/$corporateId/accounts/$accountNumber";
|
||||||
|
$timestamp = now()->format('Y-m-d\TH:i:s.vP');
|
||||||
|
$body = '';
|
||||||
|
|
||||||
|
$signature = $this->generateSignature(
|
||||||
|
$method,
|
||||||
|
$relativeUrl,
|
||||||
|
$token,
|
||||||
|
$body,
|
||||||
|
$timestamp
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = Http::withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $token,
|
||||||
|
'X-BCA-Key' => $this->apiKey,
|
||||||
|
'X-BCA-Timestamp' => $timestamp,
|
||||||
|
'X-BCA-Signature' => $signature,
|
||||||
|
])->get($this->baseUrl . $relativeUrl);
|
||||||
|
|
||||||
|
return $response->json();
|
||||||
|
}
|
||||||
|
}
|
||||||
102
app/Services/BcaSignatureService.php
Normal file
102
app/Services/BcaSignatureService.php
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
// use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class BcaSignatureService
|
||||||
|
{
|
||||||
|
|
||||||
|
public function generateSignature($method, $relativeUrl, $accessToken, $body, $timestamp, $apiSecret)
|
||||||
|
{
|
||||||
|
$bodyHash = hash('sha256', $body);
|
||||||
|
|
||||||
|
$stringToSign = strtoupper($method) . ':' .
|
||||||
|
$relativeUrl . ':' .
|
||||||
|
$accessToken . ':' .
|
||||||
|
strtolower($bodyHash) . ':' .
|
||||||
|
$timestamp;
|
||||||
|
|
||||||
|
return hash_hmac('sha256', $stringToSign, $apiSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
// public function test_generate_signature_post()
|
||||||
|
// {
|
||||||
|
// $method = 'POST';
|
||||||
|
// $relativeUrl = '/test/api';
|
||||||
|
// $accessToken = 'dummy_token';
|
||||||
|
// $timestamp = '2026-04-10T10:00:00.000+07:00';
|
||||||
|
// $apiSecret = 'secret123';
|
||||||
|
|
||||||
|
// $body = json_encode([
|
||||||
|
// "foo" => "bar"
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// $signature = $this->generateSignature(
|
||||||
|
// $method,
|
||||||
|
// $relativeUrl,
|
||||||
|
// $accessToken,
|
||||||
|
// $body,
|
||||||
|
// $timestamp,
|
||||||
|
// $apiSecret
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // Expected manual calculation (hardcode hasil dari tool / Postman)
|
||||||
|
// $expected = hash_hmac('sha256',
|
||||||
|
// 'POST:/test/api:dummy_token:' . hash('sha256', $body) . ':' . $timestamp,
|
||||||
|
// $apiSecret
|
||||||
|
// );
|
||||||
|
|
||||||
|
// $this->assertEquals($expected, $signature);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public function test_generate_signature_get_empty_body()
|
||||||
|
// {
|
||||||
|
// $method = 'GET';
|
||||||
|
// $relativeUrl = '/test/api';
|
||||||
|
// $accessToken = 'dummy_token';
|
||||||
|
// $timestamp = '2026-04-10T10:00:00.000+07:00';
|
||||||
|
// $apiSecret = 'secret123';
|
||||||
|
|
||||||
|
// $body = '';
|
||||||
|
|
||||||
|
// $signature = $this->generateSignature(
|
||||||
|
// $method,
|
||||||
|
// $relativeUrl,
|
||||||
|
// $accessToken,
|
||||||
|
// $body,
|
||||||
|
// $timestamp,
|
||||||
|
// $apiSecret
|
||||||
|
// );
|
||||||
|
|
||||||
|
// $expectedBodyHash = hash('sha256', '');
|
||||||
|
|
||||||
|
// $expected = hash_hmac('sha256',
|
||||||
|
// 'GET:/test/api:dummy_token:' . $expectedBodyHash . ':' . $timestamp,
|
||||||
|
// $apiSecret
|
||||||
|
// );
|
||||||
|
|
||||||
|
// $this->assertEquals($expected, $signature);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public function test_string_to_sign_format()
|
||||||
|
// {
|
||||||
|
// $method = 'post'; // sengaja lowercase
|
||||||
|
// $relativeUrl = '/test/api';
|
||||||
|
// $accessToken = 'token';
|
||||||
|
// $timestamp = '2026-04-10T10:00:00.000+07:00';
|
||||||
|
// $body = '{"a":1}';
|
||||||
|
|
||||||
|
// $bodyHash = hash('sha256', $body);
|
||||||
|
|
||||||
|
// $stringToSign = strtoupper($method) . ':' .
|
||||||
|
// $relativeUrl . ':' .
|
||||||
|
// $accessToken . ':' .
|
||||||
|
// strtolower($bodyHash) . ':' .
|
||||||
|
// $timestamp;
|
||||||
|
|
||||||
|
// $this->assertStringStartsWith('POST:', $stringToSign);
|
||||||
|
// $this->assertStringContainsString($relativeUrl, $stringToSign);
|
||||||
|
// $this->assertStringContainsString($accessToken, $stringToSign);
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -8866,6 +8866,29 @@ sup {
|
|||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.breadcrumb ol {
|
||||||
|
display: flex;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb li + li::before {
|
||||||
|
content: "/"; /* Standard separator; can also use ">" */
|
||||||
|
padding: 0 8px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.breadcrumb a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #0275d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
sub {
|
sub {
|
||||||
bottom: -.25em;
|
bottom: -.25em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,11 +43,19 @@
|
|||||||
<div class="container-fluid page-body-wrapper">
|
<div class="container-fluid page-body-wrapper">
|
||||||
<div class="main-panel">
|
<div class="main-panel">
|
||||||
<div class="content-wrapper">
|
<div class="content-wrapper">
|
||||||
|
<nav aria-label="Breadcrumb" class="breadcrumb">
|
||||||
|
<ol>
|
||||||
|
<li><a href="/">Home</a></li>
|
||||||
|
<li><a href="/products">Laporan</a></li>
|
||||||
|
<li><a href="#">Parkir Per Pintu</a></li>
|
||||||
|
{{-- <li><span aria-current="page"></span></li> --}}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12 grid-margin stretch-card">
|
<div class="col-md-12 grid-margin stretch-card">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h4 class="card-title">Laporan Per Pintu ({{ $locationSettings->namalokasi }})</h4>
|
<h4 class="card-title laporan">Laporan Per Pintu ({{ $locationSettings->namalokasi }})</h4>
|
||||||
<form id="fromByGate">
|
<form id="fromByGate">
|
||||||
@csrf
|
@csrf
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -64,7 +72,7 @@
|
|||||||
<div class="col-sm-12 col-md-2">
|
<div class="col-sm-12 col-md-2">
|
||||||
<label class="label" for="status_trans">Status Transaksi</label>
|
<label class="label" for="status_trans">Status Transaksi</label>
|
||||||
<select id="status_transaksi" name="status_transaksi" class="form-control select2" data-placeholder="Semua">
|
<select id="status_transaksi" name="status_transaksi" class="form-control select2" data-placeholder="Semua">
|
||||||
<option value="null">Semua</option>
|
<option value="">Semua</option>
|
||||||
<option value="0">Casual</option>
|
<option value="0">Casual</option>
|
||||||
<option value="3">Member</option>
|
<option value="3">Member</option>
|
||||||
<option value="-1">Batal</option>
|
<option value="-1">Batal</option>
|
||||||
@@ -87,7 +95,7 @@
|
|||||||
<div class="col-sm-12 col-md-2">
|
<div class="col-sm-12 col-md-2">
|
||||||
<label class="label" for="cara_bayar">Metode Pembayaran</label>
|
<label class="label" for="cara_bayar">Metode Pembayaran</label>
|
||||||
<select id="cara_bayar" name="cara_bayar" class="form-control select2" data-placeholder="Semua">
|
<select id="cara_bayar" name="cara_bayar" class="form-control select2" data-placeholder="Semua">
|
||||||
<option value="null">Semua</option>
|
<option value="">Semua</option>
|
||||||
<option value="cash">Tunai</option>
|
<option value="cash">Tunai</option>
|
||||||
<option value="cashless">Non Tunai</option>
|
<option value="cashless">Non Tunai</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -113,33 +121,26 @@
|
|||||||
<select id="cari_petugas" class="form-control js-example-basic-multiple" name="cariPetugas" data-placeholder="Petugas (Semua)" multiple="multiple">
|
<select id="cari_petugas" class="form-control js-example-basic-multiple" name="cariPetugas" data-placeholder="Petugas (Semua)" multiple="multiple">
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-sm-12 col-md-2">
|
||||||
|
<label class="label" for="theme">Template</label>
|
||||||
|
<select id="theme" class="form-control select2" data-placeholder="Default">
|
||||||
|
<option value="">Default</option>
|
||||||
|
<option value="2">Mandiri</option>
|
||||||
|
<option value="3">Mobile</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<!-- Tombol -->
|
<!-- Tombol -->
|
||||||
<div class="col-sm-12 col-md-2 d-flex gap-2 align-items-end">
|
<div class="col-sm-12 col-md-2 d-flex gap-2 align-items-end">
|
||||||
<button type="button" class="btn btn-info btn-icon-text btn-export-pdf w-100">
|
<button type="button" class="btn btn-info btn-icon-text btn-export-pdf w-100">
|
||||||
Print <i class="mdi mdi-printer btn-icon-append"></i>
|
Save To PDF <i class="mdi mdi-pdf btn-icon-append"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{{-- <div class="col-xl-12">
|
<div class="card-body">
|
||||||
<div class="card overflow-hidden">
|
<div class="table-responsive" style="display: none">
|
||||||
<div class="card-header d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-2">
|
|
||||||
<div>
|
|
||||||
<h1 class="card-title">TRANSAKSI TUNAI</h1>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex align-items-center mb-3 flex-wrap gap-2">
|
|
||||||
<div class=""><p class="text-muted m-0">Transaksi Tanggal : </p></div>
|
|
||||||
<div class=""> <p class="text-muted m-0">Filter by hari</p></div>
|
|
||||||
<div class="toggle toggle-primary off mb-0">
|
|
||||||
<span></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div> --}}
|
|
||||||
{{-- <div class="table-responsive">
|
|
||||||
<table class="table table-bordered text-center" id="tableGate">
|
<table class="table table-bordered text-center" id="tableGate">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -151,52 +152,46 @@
|
|||||||
<th>Pendapatan</th>
|
<th>Pendapatan</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody id="tableGateBody"></tbody>
|
||||||
<!-- isi data -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
</table>
|
||||||
</div
|
<div class="col-xl-12">
|
||||||
<hr>
|
<tfoot id="tableGateFooter"></tfoot>
|
||||||
<div class="card-body">
|
<div class="card overflow-hidden">
|
||||||
<div id="gateContainer" class="mt-3"></div>
|
<div class="card-header d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-2">
|
||||||
</div> --}}
|
<div>
|
||||||
{{-- <div class="container-fluid">
|
<h1 class="card-title">Total Transaksi</h1>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center mb-3 flex-wrap gap-2">
|
||||||
|
<div class=""><p class="text-muted m-0">Transaksi Tanggal : </p></div>
|
||||||
|
<div class=""><p class="text-muted m-0">Filter by hari</p></div>
|
||||||
|
<div class="toggle toggle-primary off mb-0">
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- KPI -->
|
{{-- <hr> --}}
|
||||||
|
|
||||||
|
<div class="container-fluid default" style="display: none">
|
||||||
<div class="row g-3 mb-3" id="summaryCards"></div>
|
<div class="row g-3 mb-3" id="summaryCards"></div>
|
||||||
|
|
||||||
<!-- DATA -->
|
|
||||||
<div id="gateContainer"></div>
|
<div id="gateContainer"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div> --}}
|
<div class="mobile" style="display: none">
|
||||||
<div class="container-fluid">
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
|
|
||||||
<!-- LEFT: LIST GATE -->
|
|
||||||
<div class="col-12 col-lg-3">
|
<div class="col-12 col-lg-3">
|
||||||
<div class="card shadow-sm h-100">
|
<div class="card shadow-sm h-100">
|
||||||
<div class="card-header fw-bold">
|
<div class="card-header fw-bold">Gate List</div>
|
||||||
Gate List
|
|
||||||
</div>
|
|
||||||
<div class="list-group list-group-flush" id="gateList"></div>
|
<div class="list-group list-group-flush" id="gateList"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- RIGHT: DETAIL -->
|
|
||||||
<div class="col-12 col-lg-9">
|
<div class="col-12 col-lg-9">
|
||||||
<div id="gateDetail"></div>
|
<div id="gateDetail"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-12">
|
|
||||||
<div class="card overflow-hidden">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center"">
|
|
||||||
<div>
|
|
||||||
<h1 class="card-title">JUMLAH TRANSAKSI</h1>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex align-items-center mb-3 flex-wrap gap-2">
|
|
||||||
<h3>Total disini</h3>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,6 +213,8 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
|
let location = @json(collect($locationSettings ?? [])->except('logo'));
|
||||||
|
let pdfData = [];
|
||||||
|
|
||||||
$('.select2').select2({
|
$('.select2').select2({
|
||||||
theme: 'bootstrap-5',
|
theme: 'bootstrap-5',
|
||||||
@@ -234,16 +231,35 @@
|
|||||||
|
|
||||||
let formData = $('#fromByGate').serialize();
|
let formData = $('#fromByGate').serialize();
|
||||||
|
|
||||||
|
// let tema = $('#theme').val();
|
||||||
|
let tema = $('#theme').val() || '';
|
||||||
|
console.log(tema);
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '{{ route("gateData") }}',
|
url: '{{ route("gateData") }}',
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
data: formData,
|
data: formData,
|
||||||
success: function (response) {
|
success: function (response) {
|
||||||
console.log(response);
|
const data = response?.byGateData;
|
||||||
// renderGateData(response.byGateData);
|
pdfData = data;
|
||||||
// renderNotTable(response.byGateData);
|
if (!data) return;
|
||||||
// renderGateDashboard(response.byGateData);
|
|
||||||
renderDashboard(response.byGateData);
|
$('.mobile').hide();
|
||||||
|
$('.default').hide();
|
||||||
|
$('.table-responsive').hide(); // reset dulu
|
||||||
|
|
||||||
|
if (tema == 3) {
|
||||||
|
$('.mobile').show();
|
||||||
|
renderMobile(data);
|
||||||
|
|
||||||
|
} else if (tema == 2) {
|
||||||
|
$('.default').show();
|
||||||
|
renderDefault(data);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$('.table-responsive').show(); // 🔥 WAJIB
|
||||||
|
renderGateData(data);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: function (xhr) {
|
error: function (xhr) {
|
||||||
console.error(xhr.responseText);
|
console.error(xhr.responseText);
|
||||||
@@ -251,7 +267,7 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderDashboard(data) {
|
function renderMobile(data) {
|
||||||
|
|
||||||
let grouped = {};
|
let grouped = {};
|
||||||
|
|
||||||
@@ -279,6 +295,7 @@ function renderDashboard(data) {
|
|||||||
|
|
||||||
let gateData = grouped[gate];
|
let gateData = grouped[gate];
|
||||||
|
|
||||||
|
// $('.gate-list').css('display', 'block;');
|
||||||
// LIST ITEM
|
// LIST ITEM
|
||||||
let item = $(`
|
let item = $(`
|
||||||
<button class="list-group-item list-group-item-action ${index === 0 ? 'active' : ''}">
|
<button class="list-group-item list-group-item-action ${index === 0 ? 'active' : ''}">
|
||||||
@@ -302,14 +319,15 @@ function renderDashboard(data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatRupiah(angka) {
|
||||||
|
return 'Rp. ' + parseInt(angka).toLocaleString('id-ID');
|
||||||
|
}
|
||||||
|
|
||||||
function renderDetail(gateData) {
|
function renderDetail(gateData) {
|
||||||
|
|
||||||
let container = $('#gateDetail');
|
let container = $('#gateDetail');
|
||||||
container.empty();
|
container.empty();
|
||||||
|
|
||||||
function formatRupiah(angka) {
|
|
||||||
return 'Rp ' + parseInt(angka).toLocaleString('id-ID');
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = `
|
let html = `
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
@@ -354,7 +372,7 @@ function formatRupiah(angka) {
|
|||||||
container.html(html);
|
container.html(html);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGateDashboard(data) {
|
function renderDefault(data) {
|
||||||
|
|
||||||
let container = $('#gateContainer');
|
let container = $('#gateContainer');
|
||||||
let summary = $('#summaryCards');
|
let summary = $('#summaryCards');
|
||||||
@@ -520,7 +538,8 @@ function kendaraanLabel(kode) {
|
|||||||
|
|
||||||
function renderGateData(data) {
|
function renderGateData(data) {
|
||||||
|
|
||||||
let tbody = $('#tableGate tbody');
|
// let tbody = $('#tableGate tbody');
|
||||||
|
let tbody = $('#tableGateBody');
|
||||||
tbody.empty();
|
tbody.empty();
|
||||||
|
|
||||||
function formatRupiah(angka) {
|
function formatRupiah(angka) {
|
||||||
@@ -530,6 +549,14 @@ function formatRupiah(angka) {
|
|||||||
// GROUPING LEVEL 3 (gate → operator → kendaraan)
|
// GROUPING LEVEL 3 (gate → operator → kendaraan)
|
||||||
let grouped = {};
|
let grouped = {};
|
||||||
|
|
||||||
|
let casualJml = 0;
|
||||||
|
let memberJml = 0;
|
||||||
|
let casualIncome = 0;
|
||||||
|
let memberIncome = 0;
|
||||||
|
let totalTrans = 0;
|
||||||
|
let totalIncome = 0;
|
||||||
|
|
||||||
|
|
||||||
data.forEach(item => {
|
data.forEach(item => {
|
||||||
let gate = item.id_pintu_keluar;
|
let gate = item.id_pintu_keluar;
|
||||||
|
|
||||||
@@ -549,6 +576,20 @@ function formatRupiah(angka) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔥 HITUNG TOTAL
|
||||||
|
if (item.status_transaksi == 0) {
|
||||||
|
casualJml++;
|
||||||
|
casualIncome += parseFloat(item.income_transaksi || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.status_transaksi == 3) {
|
||||||
|
memberJml++;
|
||||||
|
memberIncome += parseFloat(item.income_transaksi || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
totalTrans++;
|
||||||
|
totalIncome += parseFloat(item.income_transaksi || 0);
|
||||||
|
|
||||||
let kendaraan = item.kendaraan;
|
let kendaraan = item.kendaraan;
|
||||||
|
|
||||||
if (!grouped[gate].operators[operator].kendaraans[kendaraan]) {
|
if (!grouped[gate].operators[operator].kendaraans[kendaraan]) {
|
||||||
@@ -564,6 +605,7 @@ function formatRupiah(angka) {
|
|||||||
let gateData = grouped[gate];
|
let gateData = grouped[gate];
|
||||||
let gateName = gateData.gateName;
|
let gateName = gateData.gateName;
|
||||||
let operators = gateData.operators;
|
let operators = gateData.operators;
|
||||||
|
let shifts = gateData.shift;
|
||||||
|
|
||||||
// total rowspan gate
|
// total rowspan gate
|
||||||
let gateRowspan = 0;
|
let gateRowspan = 0;
|
||||||
@@ -596,6 +638,8 @@ function formatRupiah(angka) {
|
|||||||
|
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
|
|
||||||
|
let shift = item.id_shift_keluar || '';
|
||||||
|
|
||||||
let status = item.status_transaksi == 0
|
let status = item.status_transaksi == 0
|
||||||
? '<span class="badge bg-success">Casual</span>'
|
? '<span class="badge bg-success">Casual</span>'
|
||||||
: '<span class="badge bg-info">Member</span>';
|
: '<span class="badge bg-info">Member</span>';
|
||||||
@@ -612,11 +656,16 @@ function formatRupiah(angka) {
|
|||||||
firstGateRow = false;
|
firstGateRow = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let operatorLabel = operatorName;
|
||||||
|
|
||||||
|
if (shift) {
|
||||||
|
operatorLabel += `<br><small class="text-bold"> (${shift})</small>`;
|
||||||
|
}
|
||||||
// OPERATOR
|
// OPERATOR
|
||||||
if (firstOperatorRow) {
|
if (firstOperatorRow) {
|
||||||
row += `
|
row += `
|
||||||
<td rowspan="${operatorRowspan}" class="align-middle">
|
<td rowspan="${operatorRowspan}" class="align-middle">
|
||||||
${operatorName}
|
${operatorLabel}
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
firstOperatorRow = false;
|
firstOperatorRow = false;
|
||||||
@@ -636,7 +685,7 @@ function formatRupiah(angka) {
|
|||||||
row += `
|
row += `
|
||||||
<td>${status}</td>
|
<td>${status}</td>
|
||||||
<td class="text-left">${item.jumlah_transaksi}</td>
|
<td class="text-left">${item.jumlah_transaksi}</td>
|
||||||
<td styles="align-text: right;">${formatRupiah(item.income_transaksi)}</td>
|
<td style="text-align: right;">${formatRupiah(item.income_transaksi)}</td>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
row += '</tr>';
|
row += '</tr>';
|
||||||
@@ -649,6 +698,32 @@ function formatRupiah(angka) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
// 🔥 SUMMARY ROW
|
||||||
|
let summaryRow = `
|
||||||
|
<tr class="fw-bold bg-light">
|
||||||
|
<td rowspan="3" colspan="3" class="align-middle text-center">
|
||||||
|
TOTAL
|
||||||
|
</td>
|
||||||
|
<td>Casual</td>
|
||||||
|
<td class="text-center">${casualJml}</td>
|
||||||
|
<td class="text-end">${formatRupiah(casualIncome)}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr class="fw-bold bg-light">
|
||||||
|
<td>Member</td>
|
||||||
|
<td class="text-center">${memberJml}</td>
|
||||||
|
<td class="text-end">${formatRupiah(memberIncome)}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr class="fw-bold bg-warning">
|
||||||
|
<td>Total</td>
|
||||||
|
<td class="text-center">${totalTrans}</td>
|
||||||
|
<td class="text-end">${formatRupiah(totalIncome)}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// append ke table
|
||||||
|
tbody.append(summaryRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNotTable(data) {
|
function renderNotTable(data) {
|
||||||
@@ -770,6 +845,220 @@ function kendaraanLabel(kode) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$('.btn-export-pdf').on('click', function () {
|
||||||
|
|
||||||
|
const title = $('.laporan').val();
|
||||||
|
const { jsPDF } = window.jspdf;
|
||||||
|
|
||||||
|
const doc = new jsPDF({
|
||||||
|
orientation: 'landscape',
|
||||||
|
unit: 'mm',
|
||||||
|
format: 'a4'
|
||||||
|
});
|
||||||
|
|
||||||
|
let jmlCasual = 0;
|
||||||
|
let jmlMember = 0;
|
||||||
|
let incomeCasual = 0;
|
||||||
|
let incomeMember = 0;
|
||||||
|
let totalTrans = 0;
|
||||||
|
let totalIncome = 0;
|
||||||
|
|
||||||
|
pdfData.forEach(row => {
|
||||||
|
|
||||||
|
totalTrans++;
|
||||||
|
totalIncome += parseFloat(row.income_transaksi || 0);
|
||||||
|
|
||||||
|
if (row.status_transaksi == 0) {
|
||||||
|
jmlCasual++;
|
||||||
|
incomeCasual += parseFloat(row.income_transaksi || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.status_transaksi == 3) {
|
||||||
|
jmlMember++;
|
||||||
|
incomeMember += parseFloat(row.income_transaksi || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageWidth = doc.internal.pageSize.getWidth();
|
||||||
|
const pageHeight = doc.internal.pageSize.getHeight();
|
||||||
|
|
||||||
|
const marginLeft = 10;
|
||||||
|
const marginRight = 10;
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// GROUPING
|
||||||
|
// =========================
|
||||||
|
let grouped = {};
|
||||||
|
|
||||||
|
pdfData.forEach(row => {
|
||||||
|
|
||||||
|
let gate = row.gate || '-';
|
||||||
|
let operator = row.petugas || '-';
|
||||||
|
|
||||||
|
if (!grouped[gate]) grouped[gate] = {};
|
||||||
|
if (!grouped[gate][operator]) grouped[gate][operator] = [];
|
||||||
|
|
||||||
|
grouped[gate][operator].push(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// BUILD TABLE
|
||||||
|
// =========================
|
||||||
|
let tableBody = [];
|
||||||
|
let no = 1;
|
||||||
|
|
||||||
|
Object.keys(grouped).forEach(gate => {
|
||||||
|
|
||||||
|
let gateFirst = true;
|
||||||
|
|
||||||
|
Object.keys(grouped[gate]).forEach(operator => {
|
||||||
|
|
||||||
|
let operatorFirst = true;
|
||||||
|
|
||||||
|
grouped[gate][operator].forEach((row) => {
|
||||||
|
|
||||||
|
tableBody.push([
|
||||||
|
no++,
|
||||||
|
gateFirst ? gate : '',
|
||||||
|
operatorFirst ? operator : '',
|
||||||
|
row.kendaraan ?? '-',
|
||||||
|
row.status_transaksi == 0 ? 'Casual' : 'Member',
|
||||||
|
'', // ❌ jumlah dihapus biar ga ngulang
|
||||||
|
formatRupiah(row.income_transaksi ?? 0),
|
||||||
|
]);
|
||||||
|
|
||||||
|
gateFirst = false;
|
||||||
|
operatorFirst = false;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// TABLE
|
||||||
|
// =========================
|
||||||
|
doc.autoTable({
|
||||||
|
|
||||||
|
head: [[
|
||||||
|
'No.',
|
||||||
|
'Pintu',
|
||||||
|
'Petugas',
|
||||||
|
'Jenis Kendaraan',
|
||||||
|
'Status',
|
||||||
|
'Jumlah',
|
||||||
|
'Pendapatan'
|
||||||
|
]],
|
||||||
|
|
||||||
|
body: tableBody,
|
||||||
|
|
||||||
|
foot: [[
|
||||||
|
{
|
||||||
|
content: 'TOTAL',
|
||||||
|
colSpan: 5,
|
||||||
|
styles: { halign: 'right', fontStyle: 'bold' }
|
||||||
|
},
|
||||||
|
totalTrans,
|
||||||
|
formatRupiah(totalIncome)
|
||||||
|
]],
|
||||||
|
|
||||||
|
showFoot: 'lastPage',
|
||||||
|
startY: 45,
|
||||||
|
|
||||||
|
styles: {
|
||||||
|
fontSize: 8,
|
||||||
|
cellPadding: 2,
|
||||||
|
valign: 'middle'
|
||||||
|
},
|
||||||
|
|
||||||
|
headStyles: {
|
||||||
|
fillColor: [220, 220, 220],
|
||||||
|
halign: 'center'
|
||||||
|
},
|
||||||
|
|
||||||
|
columnStyles: {
|
||||||
|
0: { halign: 'center', cellWidth: 10 },
|
||||||
|
1: { halign: 'left' },
|
||||||
|
2: { halign: 'left' },
|
||||||
|
3: { halign: 'left' },
|
||||||
|
4: { halign: 'center' },
|
||||||
|
5: { halign: 'center' },
|
||||||
|
6: { halign: 'right' }
|
||||||
|
},
|
||||||
|
|
||||||
|
didDrawPage: function (data) {
|
||||||
|
|
||||||
|
// ================= HEADER =================
|
||||||
|
if (data.pageNumber === 1) {
|
||||||
|
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setFontSize(14);
|
||||||
|
doc.text(String(title || 'LAPORAN'), pageWidth / 2, 15, { align: 'center' });
|
||||||
|
|
||||||
|
doc.setFontSize(10);
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.text(String(location?.namaperusahaan || '-'), pageWidth / 2, 22, { align: 'center' });
|
||||||
|
|
||||||
|
doc.setFontSize(9);
|
||||||
|
doc.text(`Periode : `, marginLeft, 32);
|
||||||
|
|
||||||
|
doc.line(marginLeft, 40, pageWidth - marginRight, 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= FOOTER =================
|
||||||
|
doc.setFontSize(8);
|
||||||
|
doc.text(
|
||||||
|
`Halaman ${data.pageNumber}`,
|
||||||
|
pageWidth / 2,
|
||||||
|
pageHeight - 10,
|
||||||
|
{ align: 'center' }
|
||||||
|
);
|
||||||
|
|
||||||
|
// ================= SUMMARY (HANYA HALAMAN TERAKHIR) =================
|
||||||
|
const pageCount = doc.internal.getNumberOfPages();
|
||||||
|
|
||||||
|
// if (data.pageNumber === pageCount) {
|
||||||
|
|
||||||
|
// let finalY = (doc.lastAutoTable && doc.lastAutoTable.finalY)
|
||||||
|
// ? doc.lastAutoTable.finalY + 10
|
||||||
|
// : 50;
|
||||||
|
|
||||||
|
// doc.setFontSize(9);
|
||||||
|
|
||||||
|
// doc.text(`Total Transaksi : ${String(totalTrans)}`, marginLeft, finalY);
|
||||||
|
// doc.text(`Total Pendapatan : ${formatRupiah(totalIncome)}`, marginLeft, finalY + 6);
|
||||||
|
|
||||||
|
// doc.text(`Casual : ${jmlCasual} (${formatRupiah(incomeCasual)})`, marginLeft, finalY + 12);
|
||||||
|
// doc.text(`Member : ${jmlMember} (${formatRupiah(incomeMember)})`, marginLeft, finalY + 18);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
|
||||||
|
});
|
||||||
|
// SUMMARY DI BAWAH (FIX)
|
||||||
|
// =========================
|
||||||
|
let finalY = doc.lastAutoTable.finalY + 10;
|
||||||
|
|
||||||
|
// kalau mentok halaman → pindah halaman baru
|
||||||
|
if (finalY > pageHeight - 30) {
|
||||||
|
doc.addPage();
|
||||||
|
finalY = 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.setFontSize(9);
|
||||||
|
doc.text(`Casual : ${jmlCasual} (${formatRupiah(incomeCasual)})`, pageWidth - marginRight, finalY + 12, { align: 'right' });
|
||||||
|
doc.text(`Member : ${jmlMember} (${formatRupiah(incomeMember)})`, pageWidth - marginRight, finalY + 18, { align: 'right' });
|
||||||
|
|
||||||
|
doc.text(`Total Transaksi : ${totalTrans}`, pageWidth - marginRight, finalY, { align: 'right' });
|
||||||
|
doc.text(`Total Pendapatan : ${formatRupiah(totalIncome)}`, pageWidth - marginRight, finalY + 6, { align: 'right' });
|
||||||
|
|
||||||
|
|
||||||
|
doc.save(`${title}.pdf`);
|
||||||
|
});
|
||||||
|
|
||||||
// function renderGateData(data) {
|
// function renderGateData(data) {
|
||||||
// let html = '';
|
// let html = '';
|
||||||
// let totalJumlah = 0;
|
// let totalJumlah = 0;
|
||||||
|
|||||||
@@ -19,6 +19,9 @@
|
|||||||
use App\Http\Controllers\StreamerController;
|
use App\Http\Controllers\StreamerController;
|
||||||
use App\Http\Controllers\Tools\StikerExtendedController;
|
use App\Http\Controllers\Tools\StikerExtendedController;
|
||||||
use App\Http\Controllers\VerifyTransController;
|
use App\Http\Controllers\VerifyTransController;
|
||||||
|
|
||||||
|
use App\Services\BcaSignatureTest;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Web Routes
|
| Web Routes
|
||||||
|
|||||||
42
tests/Unit/BcaSignatureTest.php
Normal file
42
tests/Unit/BcaSignatureTest.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit;
|
||||||
|
|
||||||
|
use Tests\TestCase;
|
||||||
|
use App\Services\BcaSignatureService;
|
||||||
|
|
||||||
|
class BcaSignatureTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_generate_signature()
|
||||||
|
{
|
||||||
|
$service = new BcaSignatureService();
|
||||||
|
|
||||||
|
$method = 'POST';
|
||||||
|
$relativeUrl = '/test/api';
|
||||||
|
$accessToken = 'dummy_token';
|
||||||
|
$timestamp = '2026-04-10T10:00:00.000+07:00';
|
||||||
|
$apiSecret = 'secret123';
|
||||||
|
|
||||||
|
$body = json_encode([
|
||||||
|
"foo" => "bar"
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $service->generateSignature(
|
||||||
|
$method,
|
||||||
|
$relativeUrl,
|
||||||
|
$accessToken,
|
||||||
|
$body,
|
||||||
|
$timestamp,
|
||||||
|
$apiSecret
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expected manual
|
||||||
|
$expected = hash_hmac(
|
||||||
|
'sha256',
|
||||||
|
'POST:/test/api:dummy_token:' . hash('sha256', $body) . ':' . $timestamp,
|
||||||
|
$apiSecret
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals($expected, $result);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user