1158 lines
49 KiB
PHP
1158 lines
49 KiB
PHP
@extends('layouts.master')
|
|
|
|
@section('styles')
|
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.8/css/jquery.dataTables.min.css">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
|
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
|
|
<link rel="stylesheet" href="{{ asset('vendors/select2/select2.min.css') }}">
|
|
<style>
|
|
.dashboard-row {
|
|
background: #f8fafc;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.dashboard-row:hover {
|
|
background: #e2e8f0;
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.card {
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.card-header {
|
|
border-radius: 12px 12px 0 0;
|
|
}
|
|
|
|
.select2-container .select2-selection--single {
|
|
height: 38px !important;
|
|
padding: 5px 10px;
|
|
}
|
|
|
|
.select2-selection__rendered {
|
|
line-height: 28px !important;
|
|
}
|
|
|
|
.select2-selection__arrow {
|
|
height: 38px !important;
|
|
}
|
|
</style>
|
|
@endsection
|
|
|
|
@section('content')
|
|
<div class="container-fluid page-body-wrapper">
|
|
<div class="main-panel">
|
|
<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="col-md-12 grid-margin stretch-card">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h4 class="card-title laporan">Laporan Per Pintu ({{ $locationSettings->namalokasi }})</h4>
|
|
<div id="user-profile" data-user-id="{{ auth()->user()->nama }}"></div>
|
|
<form id="fromByGate">
|
|
@csrf
|
|
<div class="form-group">
|
|
<!-- Baris 1 -->
|
|
<div class="row g-2 align-items-end mb-2">
|
|
<div class="col-sm-12 col-md-2">
|
|
<label class="label" for="tanggalMulai">Tanggal Mulai</label>
|
|
<input type="date" class="form-control" id="tanggalMulai" name="tanggal_mulai">
|
|
</div>
|
|
<div class="col-sm-12 col-md-2">
|
|
<label class="label" for="tanggalSelesai">Tanggal Selesai</label>
|
|
<input type="date" class="form-control" id="tanggalSelesai" name="tanggal_selesai">
|
|
</div>
|
|
<div class="col-sm-12 col-md-2">
|
|
<label class="label" for="status_trans">Status Transaksi</label>
|
|
<select id="status_transaksi" name="status_transaksi" class="form-control select2">
|
|
<option value="" selected>Semua</option>
|
|
<option value="0">Casual</option>
|
|
<option value="3">Member</option>
|
|
<option value="-1">Batal</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-sm-12 col-md-2 d-flex align-items-end gap-2">
|
|
<label class="label" class="mb-0">Shift</label>
|
|
<label class="toggle-switch toggle-switch-success mb-1">
|
|
<input type="checkbox" id="harianToggle" name="shift" checked>
|
|
<span class="toggle-slider round"></span>
|
|
</label>
|
|
</div>
|
|
<div class="col-sm-12 col-md-2 d-flex gap-2 align-items-end">
|
|
<button type="button" id="btnProses" class="btn btn-light btn-icon-text w-100">Proses</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Baris 2 -->
|
|
<div class="row g-2 align-items-end">
|
|
<div class="col-sm-12 col-md-2">
|
|
<label class="label" for="cara_bayar">Metode Pembayaran</label>
|
|
<select id="cara_bayar" name="cara_bayar" class="form-control select2" data-placeholder="Semua">
|
|
<option value="">Semua</option>
|
|
<option value="cash">Tunai</option>
|
|
<option value="cashless">Non Tunai</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-sm-12 col-md-2">
|
|
<label class="label" for="payment_method">Jenis Pembayaran</label>
|
|
<select id="payment_method" name="payment_method" class="form-control select2" data-placeholder="Semua">
|
|
<option value="null">Semua</option>
|
|
<option value="2">Mandiri</option>
|
|
<option value="3">BRI</option>
|
|
<option value="4">BNI</option>
|
|
<option value="5">BCA</option>
|
|
<option value="9">QRIS</option>
|
|
</select>
|
|
</div>
|
|
{{-- <div class="col-sm-6 col-md-2">
|
|
<label class="label" for="id_pintu">Pintu</label>
|
|
<select id="id_pintu" class="form-control js-example-basic-multiple" data-placeholder="Semua Pintu" multiple="multiple">
|
|
</select>
|
|
</div> --}}
|
|
<div class="col-sm-12 col-md-2">
|
|
<label class="label" for="cari_petugas">Petugas</label>
|
|
<select id="cari_petugas" class="form-control js-example-basic-multiple" name="cariPetugas" data-placeholder="Petugas (Semua)" multiple="multiple">
|
|
</select>
|
|
</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 -->
|
|
<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">
|
|
Save To PDF <i class="mdi mdi-pdf btn-icon-append"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive" style="display: none">
|
|
<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 id="tableGateBody"></tbody>
|
|
</table>
|
|
<div class="col-xl-12">
|
|
<tfoot id="tableGateFooter"></tfoot>
|
|
<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">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>
|
|
|
|
{{-- <hr> --}}
|
|
|
|
<div class="container-fluid default" style="display: none">
|
|
<div class="row g-3 mb-3" id="summaryCards"></div>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endsection
|
|
|
|
@section('javascript')
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
|
<script src="https://cdn.datatables.net/1.13.8/js/jquery.dataTables.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.31/jspdf.plugin.autotable.min.js"></script>
|
|
<script src="{{ asset('vendors/select2/select2.min.js') }}"></script>
|
|
<script src="{{ asset('js/select2.js') }}"></script>
|
|
|
|
<script>
|
|
$(document).ready(function () {
|
|
let location = @json(collect($locationSettings ?? [])->except('logo'));
|
|
let user = $('#user-profile').data('user-id');
|
|
console.log(user);
|
|
let tanggal = '';
|
|
let pdfData = [];
|
|
// const title = $('.laporan').val();
|
|
|
|
|
|
$('.select2').select2({
|
|
theme: 'bootstrap-5',
|
|
width: '100%',
|
|
placeholder: "Semua",
|
|
allowClear: true,
|
|
minimumResultsForSearch: Infinity
|
|
});
|
|
|
|
$('#tanggalMulai').val(new Date().toISOString().split('T')[0]);
|
|
$('#tanggalSelesai').val(new Date().toISOString().split('T')[0]);
|
|
|
|
|
|
$('#btnProses').on('click', function () {
|
|
|
|
let formData = $('#fromByGate').serialize();
|
|
|
|
let startDate = $('#tanggalMulai').val();
|
|
let endDate = $('#tanggalSelesai').val();
|
|
|
|
if ( startDate !== endDate ) {
|
|
tanggal = startDate + ' s/d ' + endDate;
|
|
} else {
|
|
tanggal = endDate;
|
|
}
|
|
|
|
console.log(tanggal);
|
|
|
|
// let tema = $('#theme').val();
|
|
let tema = $('#theme').val() || '';
|
|
console.log(tema);
|
|
|
|
$.ajax({
|
|
url: '{{ route("gateData") }}',
|
|
type: 'GET',
|
|
data: formData,
|
|
success: function (response) {
|
|
const data = response?.byGateData;
|
|
pdfData = data;
|
|
if (!data) return;
|
|
|
|
$('.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) {
|
|
console.error(xhr.responseText);
|
|
}
|
|
});
|
|
});
|
|
|
|
function renderMobile(data) {
|
|
|
|
let grouped = {};
|
|
|
|
// GROUPING
|
|
data.forEach(item => {
|
|
let gate = item.id_pintu_keluar;
|
|
|
|
if (!grouped[gate]) {
|
|
grouped[gate] = {
|
|
name: item.gate,
|
|
data: []
|
|
};
|
|
}
|
|
|
|
grouped[gate].data.push(item);
|
|
});
|
|
|
|
let gateList = $('#gateList');
|
|
let gateDetail = $('#gateDetail');
|
|
|
|
gateList.empty();
|
|
gateDetail.empty();
|
|
|
|
Object.keys(grouped).forEach((gate, index) => {
|
|
|
|
let gateData = grouped[gate];
|
|
|
|
// $('.gate-list').css('display', 'block;');
|
|
// LIST ITEM
|
|
let item = $(`
|
|
<button class="list-group-item list-group-item-action ${index === 0 ? 'active' : ''}">
|
|
<div class="d-flex justify-content-between">
|
|
<span>${gateData.name}</span>
|
|
<small>${gate}</small>
|
|
</div>
|
|
</button>
|
|
`);
|
|
|
|
item.on('click', function() {
|
|
$('.list-group-item').removeClass('active');
|
|
$(this).addClass('active');
|
|
renderDetail(gateData);
|
|
});
|
|
|
|
gateList.append(item);
|
|
|
|
// auto load pertama
|
|
if (index === 0) renderDetail(gateData);
|
|
});
|
|
}
|
|
|
|
function formatRupiah(angka) {
|
|
return 'Rp. ' + parseInt(angka).toLocaleString('id-ID');
|
|
}
|
|
|
|
function renderDetail(gateData) {
|
|
|
|
let container = $('#gateDetail');
|
|
container.empty();
|
|
|
|
|
|
let html = `
|
|
<div class="card shadow-sm">
|
|
<div class="card-header bg-dark text-white">
|
|
${gateData.name}
|
|
</div>
|
|
<div class="card-body">
|
|
`;
|
|
|
|
let grouped = {};
|
|
|
|
// group by operator
|
|
gateData.data.forEach(item => {
|
|
if (!grouped[item.petugas]) grouped[item.petugas] = [];
|
|
grouped[item.petugas].push(item);
|
|
});
|
|
|
|
Object.keys(grouped).forEach(operator => {
|
|
|
|
html += `<div class="mb-3"><strong>👤 ${operator}</strong></div>`;
|
|
|
|
grouped[operator].forEach(item => {
|
|
|
|
let status = item.status_transaksi == 0 ? 'Casual' : 'Member';
|
|
|
|
html += `
|
|
<div class="d-flex justify-content-between border-bottom py-2">
|
|
<div>
|
|
${item.kendaraan} - ${status}
|
|
</div>
|
|
<div>
|
|
${item.jumlah_transaksi} trx |
|
|
<strong>${formatRupiah(item.income_transaksi)}</strong>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
});
|
|
|
|
html += `</div></div>`;
|
|
|
|
container.html(html);
|
|
}
|
|
|
|
function renderDefault(data) {
|
|
|
|
let container = $('#gateContainer');
|
|
let summary = $('#summaryCards');
|
|
|
|
container.empty();
|
|
summary.empty();
|
|
|
|
function formatRupiah(angka) {
|
|
return 'Rp ' + parseInt(angka).toLocaleString('id-ID');
|
|
}
|
|
|
|
function kendaraanLabel(kode) {
|
|
return {
|
|
'A': 'Mobil',
|
|
'C': 'Motor',
|
|
'T': 'Truck'
|
|
}[kode] || kode;
|
|
}
|
|
|
|
// =========================
|
|
// 🔢 HITUNG SUMMARY
|
|
// =========================
|
|
let totalTransaksi = 0;
|
|
let totalIncome = 0;
|
|
|
|
data.forEach(item => {
|
|
totalTransaksi += item.jumlah_transaksi;
|
|
totalIncome += parseInt(item.income_transaksi);
|
|
});
|
|
|
|
summary.append(`
|
|
<div class="col-6 col-md-3">
|
|
<div class="card shadow-sm border-0 p-3 text-center">
|
|
<h6>Total Transaksi</h6>
|
|
<h4>${totalTransaksi}</h4>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-6 col-md-3">
|
|
<div class="card shadow-sm border-0 p-3 text-center">
|
|
<h6>Total Pendapatan</h6>
|
|
<h4 class="text-success">${formatRupiah(totalIncome)}</h4>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-6 col-md-3">
|
|
<div class="card shadow-sm border-0 p-3 text-center">
|
|
<h6>Total Gate</h6>
|
|
<h4>${[...new Set(data.map(d => d.id_pintu_keluar))].length}</h4>
|
|
</div>
|
|
</div>
|
|
`);
|
|
|
|
// =========================
|
|
// 🧠 GROUPING
|
|
// =========================
|
|
let grouped = {};
|
|
|
|
data.forEach(item => {
|
|
let gate = item.id_pintu_keluar;
|
|
|
|
if (!grouped[gate]) {
|
|
grouped[gate] = {
|
|
gateName: item.gate,
|
|
operators: {}
|
|
};
|
|
}
|
|
|
|
let operator = item.petugas;
|
|
|
|
if (!grouped[gate].operators[operator]) {
|
|
grouped[gate].operators[operator] = {
|
|
name: operator,
|
|
kendaraans: {}
|
|
};
|
|
}
|
|
|
|
let kendaraan = item.kendaraan;
|
|
|
|
if (!grouped[gate].operators[operator].kendaraans[kendaraan]) {
|
|
grouped[gate].operators[operator].kendaraans[kendaraan] = [];
|
|
}
|
|
|
|
grouped[gate].operators[operator].kendaraans[kendaraan].push(item);
|
|
});
|
|
|
|
// =========================
|
|
// 🎨 RENDER UI
|
|
// =========================
|
|
Object.keys(grouped).forEach(gate => {
|
|
|
|
let gateData = grouped[gate];
|
|
|
|
let gateCard = $(`
|
|
<div class="card shadow border-0 mb-4">
|
|
<div class="card-header d-flex justify-content-between align-items-center bg-dark text-white">
|
|
<div>
|
|
<strong>${gateData.gateName}</strong><br>
|
|
<small>${gate}</small>
|
|
</div>
|
|
<span class="badge bg-info">Gate</span>
|
|
</div>
|
|
<div class="card-body"></div>
|
|
</div>
|
|
`);
|
|
|
|
let gateBody = gateCard.find('.card-body');
|
|
|
|
Object.values(gateData.operators).forEach(operatorData => {
|
|
|
|
let operatorBlock = $(`
|
|
<div class="mb-4">
|
|
<div class="fw-bold mb-2">
|
|
👤 ${operatorData.name}
|
|
</div>
|
|
</div>
|
|
`);
|
|
|
|
Object.keys(operatorData.kendaraans).forEach(kendaraan => {
|
|
|
|
let kendaraanBlock = $(`
|
|
<div class="mb-2 ms-md-3">
|
|
<div class="text-primary fw-semibold mb-2">
|
|
🚗 ${kendaraanLabel(kendaraan)}
|
|
</div>
|
|
</div>
|
|
`);
|
|
|
|
operatorData.kendaraans[kendaraan].forEach(item => {
|
|
|
|
let statusClass = item.status_transaksi == 0 ? 'success' : 'info';
|
|
let statusText = item.status_transaksi == 0 ? 'Casual' : 'Member';
|
|
|
|
let row = $(`
|
|
<div class="dashboard-row p-2 mb-2 rounded d-flex flex-column flex-md-row justify-content-between align-items-md-center">
|
|
|
|
<div>
|
|
<span class="badge bg-${statusClass}">${statusText}</span>
|
|
</div>
|
|
|
|
<div class="d-flex gap-3 mt-2 mt-md-0">
|
|
<div>${item.jumlah_transaksi} trx</div>
|
|
<div class="fw-bold text-success">
|
|
${formatRupiah(item.income_transaksi)}
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
`);
|
|
|
|
kendaraanBlock.append(row);
|
|
});
|
|
|
|
operatorBlock.append(kendaraanBlock);
|
|
});
|
|
|
|
gateBody.append(operatorBlock);
|
|
});
|
|
|
|
container.append(gateCard);
|
|
});
|
|
}
|
|
|
|
function renderGateData(data) {
|
|
|
|
// let tbody = $('#tableGate tbody');
|
|
let tbody = $('#tableGateBody');
|
|
tbody.empty();
|
|
|
|
function formatRupiah(angka) {
|
|
return 'Rp ' + parseInt(angka).toLocaleString('id-ID');
|
|
}
|
|
|
|
// GROUPING LEVEL 3 (gate → operator → kendaraan)
|
|
let grouped = {};
|
|
|
|
let casualJml = 0;
|
|
let memberJml = 0;
|
|
let casualIncome = 0;
|
|
let memberIncome = 0;
|
|
let totalTrans = 0;
|
|
let totalIncome = 0;
|
|
|
|
|
|
data.forEach(item => {
|
|
let gate = item.id_pintu_keluar;
|
|
|
|
if (!grouped[gate]) {
|
|
grouped[gate] = {
|
|
gateName: item.gate,
|
|
operators: {}
|
|
};
|
|
}
|
|
|
|
// let operator = item.petugas;
|
|
let operator = (item.petugas && item.petugas.trim() !== null)
|
|
? item.petugas
|
|
: 'NO-OP';
|
|
|
|
if (!grouped[gate].operators[operator]) {
|
|
grouped[gate].operators[operator] = {
|
|
name: operator,
|
|
kendaraans: {}
|
|
};
|
|
}
|
|
|
|
// 🔥 HITUNG TOTAL
|
|
if (item.status_transaksi == 0) {
|
|
casualJml += parseFloat(item.jumlah_transaksi || 0);
|
|
casualIncome += parseFloat(item.income_transaksi || 0);
|
|
}
|
|
|
|
if (item.status_transaksi == 3) {
|
|
memberJml += parseFloat(item.jumlah_transaksi || 0);
|
|
memberIncome += parseFloat(item.income_transaksi || 0);
|
|
}
|
|
|
|
totalTrans += parseFloat(item.jumlah_transaksi || 0);
|
|
totalIncome += parseFloat(item.income_transaksi || 0);
|
|
|
|
let kendaraan = item.kendaraan;
|
|
|
|
if (!grouped[gate].operators[operator].kendaraans[kendaraan]) {
|
|
grouped[gate].operators[operator].kendaraans[kendaraan] = [];
|
|
}
|
|
|
|
grouped[gate].operators[operator].kendaraans[kendaraan].push(item);
|
|
});
|
|
|
|
// RENDER
|
|
Object.keys(grouped).forEach(gate => {
|
|
|
|
let gateData = grouped[gate];
|
|
let gateName = gateData.gateName;
|
|
let operators = gateData.operators;
|
|
let shifts = gateData.shift;
|
|
|
|
// total rowspan gate
|
|
let gateRowspan = 0;
|
|
Object.values(operators).forEach(op => {
|
|
Object.values(op.kendaraans).forEach(k => {
|
|
gateRowspan += k.length;
|
|
});
|
|
});
|
|
|
|
let firstGateRow = true;
|
|
|
|
Object.values(operators).forEach(operatorData => {
|
|
|
|
let operatorName = operatorData.name;
|
|
let kendaraans = operatorData.kendaraans;
|
|
|
|
// total rowspan operator
|
|
let operatorRowspan = 0;
|
|
Object.values(kendaraans).forEach(k => {
|
|
operatorRowspan += k.length;
|
|
});
|
|
|
|
let firstOperatorRow = true;
|
|
|
|
Object.keys(kendaraans).forEach(kendaraan => {
|
|
|
|
let items = kendaraans[kendaraan];
|
|
let kendaraanRowspan = items.length;
|
|
let firstKendaraanRow = true;
|
|
|
|
items.forEach(item => {
|
|
|
|
let shift = item.id_shift_keluar || '';
|
|
|
|
let status = item.status_transaksi == 0
|
|
? '<span class="badge bg-success">Casual</span>'
|
|
: '<span class="badge bg-info">Member</span>';
|
|
|
|
let row = '<tr>';
|
|
|
|
// GATE
|
|
if (firstGateRow) {
|
|
row += `
|
|
<td rowspan="${gateRowspan}" class="align-middle fw-bold">
|
|
${gateData.gateName}<br><small>(${gate})</small>
|
|
</td>
|
|
`;
|
|
firstGateRow = false;
|
|
}
|
|
|
|
let operatorLabel = operatorName;
|
|
|
|
if (shift) {
|
|
operatorLabel += `<br><small class="text-bold"> (${shift})</small>`;
|
|
}
|
|
// OPERATOR
|
|
if (firstOperatorRow) {
|
|
row += `
|
|
<td rowspan="${operatorRowspan}" class="align-middle">
|
|
${operatorLabel}
|
|
</td>
|
|
`;
|
|
firstOperatorRow = false;
|
|
}
|
|
|
|
// KENDARAAN (NEW 🔥)
|
|
if (firstKendaraanRow) {
|
|
row += `
|
|
<td rowspan="${kendaraanRowspan}" class="align-middle">
|
|
${kendaraan}
|
|
</td>
|
|
`;
|
|
firstKendaraanRow = false;
|
|
}
|
|
|
|
// DATA
|
|
row += `
|
|
<td>${status}</td>
|
|
<td class="text-left">${item.jumlah_transaksi}</td>
|
|
<td style="text-align: right;">${formatRupiah(item.income_transaksi)}</td>
|
|
`;
|
|
|
|
row += '</tr>';
|
|
|
|
tbody.append(row);
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
// 🔥 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) {
|
|
|
|
let container = $('#gateContainer');
|
|
container.empty();
|
|
|
|
function formatRupiah(angka) {
|
|
return 'Rp ' + parseInt(angka).toLocaleString('id-ID');
|
|
}
|
|
|
|
function kendaraanLabel(kode) {
|
|
return {
|
|
'A': 'Mobil',
|
|
'C': 'Motor',
|
|
'T': 'Truck'
|
|
}[kode] || kode;
|
|
}
|
|
|
|
// GROUPING
|
|
let grouped = {};
|
|
|
|
data.forEach(item => {
|
|
let gate = item.id_pintu_keluar;
|
|
|
|
if (!grouped[gate]) {
|
|
grouped[gate] = {
|
|
gateName: item.gate,
|
|
operators: {}
|
|
};
|
|
}
|
|
|
|
// let operator = item.petugas || 'NO-OP';
|
|
let operator = (item.petugas && item.petugas.trim() !== '')
|
|
? item.petugas
|
|
: 'NO-OP';
|
|
|
|
if (!grouped[gate].operators[operator]) {
|
|
grouped[gate].operators[operator] = {
|
|
name: operator,
|
|
kendaraans: {}
|
|
};
|
|
}
|
|
|
|
let kendaraan = item.kendaraan;
|
|
|
|
if (!grouped[gate].operators[operator].kendaraans[kendaraan]) {
|
|
grouped[gate].operators[operator].kendaraans[kendaraan] = [];
|
|
}
|
|
|
|
grouped[gate].operators[operator].kendaraans[kendaraan].push(item);
|
|
});
|
|
|
|
// RENDER
|
|
Object.keys(grouped).forEach(gate => {
|
|
|
|
let gateData = grouped[gate];
|
|
|
|
let gateCard = $(`
|
|
<div class="card shadow-sm mb-3">
|
|
<div class="card-header bg-primary text-white">
|
|
<strong>${gateData.gateName}</strong>
|
|
<small>(${gate})</small>
|
|
</div>
|
|
<div class="card-body"></div>
|
|
</div>
|
|
`);
|
|
|
|
let gateBody = gateCard.find('.card-body');
|
|
|
|
Object.values(gateData.operators).forEach(operatorData => {
|
|
|
|
let operatorBlock = $(`
|
|
<div class="mb-3 p-2 border rounded">
|
|
<div class="fw-bold text-dark mb-2">
|
|
👤 ${operatorData.name}
|
|
</div>
|
|
</div>
|
|
`);
|
|
|
|
Object.keys(operatorData.kendaraans).forEach(kendaraan => {
|
|
|
|
let kendaraanBlock = $(`
|
|
<div class="mb-2 ms-md-3">
|
|
<div class="text-primary fw-semibold mb-1">
|
|
🚗 ${kendaraanLabel(kendaraan)}
|
|
</div>
|
|
</div>
|
|
`);
|
|
|
|
operatorData.kendaraans[kendaraan].forEach(item => {
|
|
|
|
let status = item.status_transaksi == 0
|
|
? '<span class="badge bg-success">Casual</span>'
|
|
: '<span class="badge bg-info">Member</span>';
|
|
|
|
let row = $(`
|
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center bg-light rounded p-2 mb-2">
|
|
|
|
<div>${status}</div>
|
|
|
|
<div class="d-flex gap-3 mt-2 mt-md-0">
|
|
<div><strong>${item.jumlah_transaksi}</strong> trx</div>
|
|
<div class="text-success fw-bold">
|
|
${formatRupiah(item.income_transaksi)}
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
`);
|
|
|
|
kendaraanBlock.append(row);
|
|
});
|
|
|
|
operatorBlock.append(kendaraanBlock);
|
|
});
|
|
|
|
gateBody.append(operatorBlock);
|
|
});
|
|
|
|
container.append(gateCard);
|
|
});
|
|
}
|
|
|
|
$('.btn-export-pdf').on('click', function () {
|
|
|
|
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 += parseFloat(row.jumlah_transaksi || 0);
|
|
totalIncome += parseFloat(row.income_transaksi || 0);
|
|
|
|
if (row.status_transaksi == 0) {
|
|
jmlCasual += parseFloat(row.jumlah_transaksi || 0);
|
|
incomeCasual += parseFloat(row.income_transaksi || 0);
|
|
}
|
|
|
|
if (row.status_transaksi == 3) {
|
|
jmlMember += parseFloat(row.jumlah_transaksi || 0);
|
|
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 || 'NO-OP';
|
|
|
|
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
|
|
row.jumlah_transaksi ?? 0,
|
|
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('LAPORAN TRANSAKSI PER PINTU'), pageWidth / 2, 15, { align: 'center' });
|
|
|
|
doc.setFontSize(12);
|
|
doc.setFont('helvetica', 'normal');
|
|
doc.text(String(tanggal || '-'), pageWidth / 2, 20, { align: 'center' });
|
|
|
|
doc.setFontSize(10);
|
|
doc.setFont('helvetica', 'normal');
|
|
doc.text(String(location?.namaperusahaan + ' - ' + location?.namalokasi || '-'), pageWidth / 2, 25, { align: 'center' });
|
|
|
|
doc.setFontSize(9);
|
|
doc.text(`Lokasi : ` + location?.namalokasi, 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(`Dicetak oleh : ${user}`, marginLeft, finalY);
|
|
doc.text(`Tanggal cetak : ${new Date().toISOString().split('T')[0]}`, marginLeft, finalY + 6);
|
|
|
|
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(`Laporan_per_pintu.pdf`);
|
|
});
|
|
|
|
// function renderGateData(data) {
|
|
// let html = '';
|
|
// let totalJumlah = 0;
|
|
// let totalIncome = 0;
|
|
|
|
// const statusLabel = { '0': 'Bayar', '3': 'Gratis' };
|
|
// const kendaraanLabel = { 'A': 'Motor', 'C': 'Mobil', 'T': 'Truk' };
|
|
|
|
// // Hitung rowspan per pintu dan per petugas
|
|
// let pintuSpan = {};
|
|
// let petugasSpan = {};
|
|
|
|
// data.forEach(function (item) {
|
|
// let pintuKey = item.id_pintu_keluar;
|
|
// let petugasKey = item.id_pintu_keluar + '_' + item.id_op_keluar;
|
|
|
|
// pintuSpan[pintuKey] = (pintuSpan[pintuKey] || 0) + 1;
|
|
// petugasSpan[petugasKey] = (petugasSpan[petugasKey] || 0) + 1;
|
|
// });
|
|
|
|
// let pintuRendered = {};
|
|
// let petugasRendered = {};
|
|
|
|
// data.forEach(function (item) {
|
|
// totalJumlah += item.jumlah_transaksi;
|
|
// totalIncome += parseInt(item.income_transaksi);
|
|
|
|
// let pintuKey = item.id_pintu_keluar;
|
|
// let petugasKey = item.id_pintu_keluar + '_' + item.id_op_keluar;
|
|
|
|
// let pintuCell = '';
|
|
// let petugasCell = '';
|
|
|
|
// if (!pintuRendered[pintuKey]) {
|
|
// pintuRendered[pintuKey] = true;
|
|
// pintuCell = `<td rowspan="${pintuSpan[pintuKey]}" style="text-align:center; vertical-align:middle;">${item.id_pintu_keluar}</td>`;
|
|
// }
|
|
|
|
// if (!petugasRendered[petugasKey]) {
|
|
// petugasRendered[petugasKey] = true;
|
|
// petugasCell = `<td rowspan="${petugasSpan[petugasKey]}" style="text-align:center; vertical-align:middle;">${item.nama}</td>`;
|
|
// }
|
|
|
|
// html += `
|
|
// <tr>
|
|
// ${pintuCell}
|
|
// ${petugasCell}
|
|
// <td style="text-align:center;">${kendaraanLabel[item.id_kendaraan] ?? item.id_kendaraan}</td>
|
|
// <td style="text-align:center;">${statusLabel[item.status_transaksi] ?? item.status_transaksi}</td>
|
|
// <td style="text-align:center;">${item.jumlah_transaksi}</td>
|
|
// <td style="text-align:right;">${formatRupiah(item.income_transaksi)}</td>
|
|
// </tr>
|
|
// `;
|
|
// });
|
|
|
|
// $('#gateDataContainer').html(html);
|
|
// $('#totalJumlah').text(totalJumlah);
|
|
// $('#totalIncome').text(formatRupiah(totalIncome));
|
|
// }
|
|
|
|
// function formatRupiah(angka) {
|
|
// return 'Rp ' + parseInt(angka).toLocaleString('id-ID');
|
|
// }
|
|
});
|
|
</script>
|
|
@endsection |