Penambahan thema pada Menu Laporan per pintu pos

This commit is contained in:
pand03
2026-04-16 17:38:48 +07:00
parent 67f6604f89
commit c73adb3c6b
8 changed files with 652 additions and 118 deletions

BIN
app/.DS_Store vendored Normal file

Binary file not shown.

View File

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

View 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();
}
}

View 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);
// }
}

View File

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

View File

@@ -7,35 +7,35 @@
<link rel="stylesheet" href="{{ asset('vendors/select2/select2.min.css') }}"> <link rel="stylesheet" href="{{ asset('vendors/select2/select2.min.css') }}">
<style> <style>
.dashboard-row { .dashboard-row {
background: #f8fafc; background: #f8fafc;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.dashboard-row:hover { .dashboard-row:hover {
background: #e2e8f0; background: #e2e8f0;
transform: translateY(-2px); transform: translateY(-2px);
} }
.card { .card {
border-radius: 12px; border-radius: 12px;
} }
.card-header { .card-header {
border-radius: 12px 12px 0 0; border-radius: 12px 12px 0 0;
} }
.select2-container .select2-selection--single { .select2-container .select2-selection--single {
height: 38px !important; height: 38px !important;
padding: 5px 10px; padding: 5px 10px;
} }
.select2-selection__rendered { .select2-selection__rendered {
line-height: 28px !important; line-height: 28px !important;
} }
.select2-selection__arrow { .select2-selection__arrow {
height: 38px !important; height: 38px !important;
} }
</style> </style>
@endsection @endsection
@@ -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,90 +121,77 @@
<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 overflow-hidden">
<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">
<thead class="table-light">
<tr>
<th>Pintu</th>
<th>Petugas</th>
<th>Jenis Kendaraan</th>
<th>Status</th>
<th>Jumlah</th>
<th>Pendapatan</th>
</tr>
</thead>
<tbody>
<!-- isi data -->
</tbody>
</table>
</div
<hr>
<div class="card-body"> <div class="card-body">
<div id="gateContainer" class="mt-3"></div> <div class="table-responsive" style="display: none">
</div> --}} <table class="table table-bordered text-center" id="tableGate">
{{-- <div class="container-fluid"> <thead class="table-light">
<tr>
<!-- KPI --> <th>Pintu</th>
<div class="row g-3 mb-3" id="summaryCards"></div> <th>Petugas</th>
<th>Jenis Kendaraan</th>
<!-- DATA --> <th>Status</th>
<div id="gateContainer"></div> <th>Jumlah</th>
<th>Pendapatan</th>
</div> --}} </tr>
<div class="container-fluid"> </thead>
<div class="row g-3"> <tbody id="tableGateBody"></tbody>
</table>
<!-- LEFT: LIST GATE --> <div class="col-xl-12">
<div class="col-12 col-lg-3"> <tfoot id="tableGateFooter"></tfoot>
<div class="card shadow-sm h-100"> <div class="card overflow-hidden">
<div class="card-header fw-bold"> <div class="card-header d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-2">
Gate List <div>
<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 class="list-group list-group-flush" id="gateList"></div>
</div> </div>
</div> </div>
<!-- RIGHT: DETAIL -->
<div class="col-12 col-lg-9">
<div id="gateDetail"></div>
</div>
</div> </div>
</div>
<div class="col-xl-12"> {{-- <hr> --}}
<div class="card overflow-hidden">
<div class="card-header d-flex justify-content-between align-items-center""> <div class="container-fluid default" style="display: none">
<div> <div class="row g-3 mb-3" id="summaryCards"></div>
<h1 class="card-title">JUMLAH TRANSAKSI</h1> <div id="gateContainer"></div>
</div>
<div class="mobile" style="display: none">
<div class="row g-3">
<div class="col-12 col-lg-3">
<div class="card shadow-sm h-100">
<div class="card-header fw-bold">Gate List</div>
<div class="list-group list-group-flush" id="gateList"></div>
</div>
</div>
<div class="col-12 col-lg-9">
<div id="gateDetail"></div>
</div> </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;

View File

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

View 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);
}
}