Skip to content

Export PDF

All in one API endpoint, no need to create a new API endpoint for each module. To be able to style the PDF file, by using the laravel blade template file. The PDF file is generated using wkhtmltopdf.

INFO

Make sure to add /api prefix in the API endpoint.

Generate PDF Endpoint

http
GET /export/pdf
GET /export/pdf

Query Parameters

NameTypeDescriptionNullable
requestsstringJSON string, contains the request datafalse

Response

200 OK

File PDF

403 Forbidden
json
{
    "message": "Anda tidak memiliki akses untuk mengunduh laporan ini"
}
{
    "message": "Anda tidak memiliki akses untuk mengunduh laporan ini"
}

Sample PDF File:

Since wkhtmltopdf is not installed on my hosting server, I can't generate the PDF file. But I have the sample PDF file that I generated from the production server E-SPBU UNDIP.

NameLinkDescription
Fund Report2023-10-25_Laporan Dana.pdfFund report on 26 October 2023 (24KB)
Sales Report2023-10-18to2023-10-25_Laporan Settlement.pdfSales report on 18 October 2023 to 25 October 2023 (28KB)
Profit Loss Report2023-01-01to2023-01-31-Laporan Laba Rugi.pdfProfit loss report on 1 January 2023 to 31 January 2023 (97KB)
Tank Delivery Report2023-09-01to2023-09-30_Laporan Bongkaran.pdfTank delivery report on 1 September 2023 to 30 September 2023 (45KB)
Pump Test Report2023-01-01to2023-09-30_Laporan Pump Test.pdfPump test report on 1 January 2023 to 30 September 2023, it generate 3260 rows of data (415KB)
Oil Movement Report2023-10-17_S3_Oil Movement Report.pdfOil movement report on 17 October 2023 3rd Shift (25KB)

Used on pages

  • /reports (Fund Report)
  • /sales (Sales Report)
  • /profitloss (Profit Loss Report)
  • /tankdeliveries (Tank Delivery Report)
  • /pumptests (Pump Test Report)
  • /opnames (Oil Movement Report)

Generate PDF Backend Implementation

  • Assume that Laravel Snappy is already installed

  • Create a route in routes/api.php

api.php
php
Route::group(['middleware' => 'auth:sanctum'], function () {
  // ..
  Route::get('export/pdf', [PrintController::class, 'exportPDF']);
})
Route::group(['middleware' => 'auth:sanctum'], function () {
  // ..
  Route::get('export/pdf', [PrintController::class, 'exportPDF']);
})
  • In the exportPDF function, I use the try-catch block to catch the exception, so the API will return the error response instead of throwing an exception.
PrintController.php
php
  public function exportPDF(Request $request)
  {
    try {
      // ..
    } catch (\Exception $exception) {
      return response()->json(['message' => $exception->getMessage()], 500);
    }
  }
  public function exportPDF(Request $request)
  {
    try {
      // ..
    } catch (\Exception $exception) {
      return response()->json(['message' => $exception->getMessage()], 500);
    }
  }
  • This method used by multiple modules, so the execution time may take long time. To prevent the request timeout, set the maximum execution time to 60 seconds, since I already tested it to generate 15.000+ rows of data of pump test report from 2021 to 2023, it only took 10-26 seconds in the staging server.
PrintController.php
php
set_time_limit(60);
ini_set('max_execution_time', 60);
ini_set('default_socket_timeout', 60);
set_time_limit(60);
ini_set('max_execution_time', 60);
ini_set('default_socket_timeout', 60);
  • Decode the request data from the query parameter
PrintController.php
php
$decodedRequest = json_decode($request->requests);
foreach ($decodedRequest as $key => $value) {
  $request[$key] = $value;
  $searchOptions[$key] = $value;
}
$decodedRequest = json_decode($request->requests);
foreach ($decodedRequest as $key => $value) {
  $request[$key] = $value;
  $searchOptions[$key] = $value;
}
  • Use conditional statement to determine which module is requested
PrintController.php
php
if ($title === 'Laporan Pump Test') {
  $pumpTestServices = new PumpTestServices;
  $pumpTestsBuilder = $pumpTestServices->indexPumpTest(2, $request);
  $pumpTestsObject = $pumpTestServices->getIndexPumpTest($pumpTestsBuilder);
  $mainData = addNumberEachRowOnGet($pumpTestsObject);
  $mainDataResource = PumpTestIndexResource::collection($pumpTestsObject)->resolve();
  $mainData = collect($mainDataResource);

  $isPumpTestBetweenDateUnvalidated = $pumpTestServices->isPumpTestBetweenDateUnvalidated($request, (int) $request->search_shift);

  if (!$isPumpTestBetweenDateUnvalidated) {
    $validator = $pumpTestServices->getUserValidator($request);
    $validatorName = $validator['checked_by'];
    $validatorSign = $validator['checked_sign'];
  }

  $isPumpTestBetweenDateUnverified = $pumpTestServices->isPumpTestBetweenDateUnverified($request, (int) $request->search_shift);

  if (!$isPumpTestBetweenDateUnverified) {
    $verifier = $pumpTestServices->getUserVerifier($request);
    $verifierName = $verifier['verified_by'];
    $verifierSign = $verifier['verified_sign'];
  }

  $pumpTestsByFuel = $pumpTestServices->indexPumpTest(1, $request);
  $pumpTestsObjectByFuel = $pumpTestServices->getIndexPumpTest($pumpTestsByFuel);

  $volume = $pumpTestServices->getTotal($pumpTestsObject, 'volume');
  $fuelValue = $pumpTestServices->getTotal($pumpTestsObject, 'fuel_value');

  $optionalData = [
    'total' => [
      'volume' => $volume,
      'fuel_value' => $fuelValue,
    ],
    'group_by_fuel' => [
      'data' => PumpTestByFuelIndexResource::collection($pumpTestsObjectByFuel)
    ],
  ];
} elseif ($title === 'Laporan Bongkaran') {
  // getting the data to get generated into PDF
} elseif ($title === 'Laporan Dana') {
  // getting the data to get generated into PDF
} elseif ($title === 'Laporan Settlement') {
  // getting the data to get generated into PDF
} elseif ($title === 'Oil Movement Report' || $title === 'Stock Opname') {
  // getting the data to get generated into PDF
} elseif ($title === 'Laporan Laba Rugi') {
  // getting the data to get generated into PDF
}
if ($title === 'Laporan Pump Test') {
  $pumpTestServices = new PumpTestServices;
  $pumpTestsBuilder = $pumpTestServices->indexPumpTest(2, $request);
  $pumpTestsObject = $pumpTestServices->getIndexPumpTest($pumpTestsBuilder);
  $mainData = addNumberEachRowOnGet($pumpTestsObject);
  $mainDataResource = PumpTestIndexResource::collection($pumpTestsObject)->resolve();
  $mainData = collect($mainDataResource);

  $isPumpTestBetweenDateUnvalidated = $pumpTestServices->isPumpTestBetweenDateUnvalidated($request, (int) $request->search_shift);

  if (!$isPumpTestBetweenDateUnvalidated) {
    $validator = $pumpTestServices->getUserValidator($request);
    $validatorName = $validator['checked_by'];
    $validatorSign = $validator['checked_sign'];
  }

  $isPumpTestBetweenDateUnverified = $pumpTestServices->isPumpTestBetweenDateUnverified($request, (int) $request->search_shift);

  if (!$isPumpTestBetweenDateUnverified) {
    $verifier = $pumpTestServices->getUserVerifier($request);
    $verifierName = $verifier['verified_by'];
    $verifierSign = $verifier['verified_sign'];
  }

  $pumpTestsByFuel = $pumpTestServices->indexPumpTest(1, $request);
  $pumpTestsObjectByFuel = $pumpTestServices->getIndexPumpTest($pumpTestsByFuel);

  $volume = $pumpTestServices->getTotal($pumpTestsObject, 'volume');
  $fuelValue = $pumpTestServices->getTotal($pumpTestsObject, 'fuel_value');

  $optionalData = [
    'total' => [
      'volume' => $volume,
      'fuel_value' => $fuelValue,
    ],
    'group_by_fuel' => [
      'data' => PumpTestByFuelIndexResource::collection($pumpTestsObjectByFuel)
    ],
  ];
} elseif ($title === 'Laporan Bongkaran') {
  // getting the data to get generated into PDF
} elseif ($title === 'Laporan Dana') {
  // getting the data to get generated into PDF
} elseif ($title === 'Laporan Settlement') {
  // getting the data to get generated into PDF
} elseif ($title === 'Oil Movement Report' || $title === 'Stock Opname') {
  // getting the data to get generated into PDF
} elseif ($title === 'Laporan Laba Rugi') {
  // getting the data to get generated into PDF
}
  • After getting the data, pass it to the blade template file also setup the layout of the PDF file
PrintController.php
php

$data = [
  'bussinessName' => $bussinessName,
  'bussinessAddress' => $bussinessAddress,
  'printTitle' => $title,
  'searchOptions' => $searchOptions,
  'shiftID' => $shift,
  'dateRange' => [
    $dateRangeStart,
    $dateRangeEnd
  ],
  'mainData' => $mainData,
  'total' => $total,
  'optionalData' => $optionalData,
  'timeNow' => now()->format('d/m/Y HH:mm'),
  'portraitTitles' => ['Laporan Dana', 'Laporan Settlement', 'Stock Opname'],
  'now' => now()->format('d/m/Y H:i'),
  'validator' => [
    'validatorName' => $validatorName,
    'validatorSign' => $validatorSign,
  ],
  'verifier' => [
    'verifierName' => $verifierName,
    'verifierSign' => $verifierSign,
  ],
  'kopSurat' => $kopSurat,
];


$filename = "$dateRange-$shiftName-$title.pdf";
view()->share('data', $data);

$paperOrientation = 'landscape';
if (in_array($title, $data['portraitTitles'])) {
  $paperOrientation = 'portrait';
}
$pdf = App::make('snappy.pdf.wrapper');
$pdf->loadView('print.print-layout', compact('data'));
$pdf->setOptions([
  'page-size' => 'a4',
  'orientation' => $paperOrientation,
  'footer-html' => view('print.print-layout-footer', compact('data')),
  'margin-top' => '10mm',
  'margin-bottom' => '10mm',
]);

return $pdf->stream($filename);

$data = [
  'bussinessName' => $bussinessName,
  'bussinessAddress' => $bussinessAddress,
  'printTitle' => $title,
  'searchOptions' => $searchOptions,
  'shiftID' => $shift,
  'dateRange' => [
    $dateRangeStart,
    $dateRangeEnd
  ],
  'mainData' => $mainData,
  'total' => $total,
  'optionalData' => $optionalData,
  'timeNow' => now()->format('d/m/Y HH:mm'),
  'portraitTitles' => ['Laporan Dana', 'Laporan Settlement', 'Stock Opname'],
  'now' => now()->format('d/m/Y H:i'),
  'validator' => [
    'validatorName' => $validatorName,
    'validatorSign' => $validatorSign,
  ],
  'verifier' => [
    'verifierName' => $verifierName,
    'verifierSign' => $verifierSign,
  ],
  'kopSurat' => $kopSurat,
];


$filename = "$dateRange-$shiftName-$title.pdf";
view()->share('data', $data);

$paperOrientation = 'landscape';
if (in_array($title, $data['portraitTitles'])) {
  $paperOrientation = 'portrait';
}
$pdf = App::make('snappy.pdf.wrapper');
$pdf->loadView('print.print-layout', compact('data'));
$pdf->setOptions([
  'page-size' => 'a4',
  'orientation' => $paperOrientation,
  'footer-html' => view('print.print-layout-footer', compact('data')),
  'margin-top' => '10mm',
  'margin-bottom' => '10mm',
]);

return $pdf->stream($filename);
blade
@extends('layouts.app')
@section('content')
  <div style="font-family: ui-serif, Georgia, Cambria, 'Times New Roman', 'Times, serif' !important">
    <div class="flex flex-col w-full font-serif">
      <div>
        <div class="flex flex-col w-full h-max" id="printable-area">
          @if (!!$data['kopSurat'])
            @include('print.print-kop-surat', ['kopSurat' => $data['kopSurat']])
          @else
            <div class="font-sans tracking-tight font-bold uppercase text-sm">{{ $data['bussinessName'] }}</div>
            <div class="font-sans tracking-tight font-bold uppercase text-sm">{{ $data['bussinessAddress'] }}</div>
          @endif
          <div class="font-semibold font-sans uppercase text-center mt-1 text-xl"
            style="letter-spacing: 0.04em !important;">
            {{ $data['printTitle'] }}
          </div>
          <div class="font-bold tracking-tight text-base">Period :
            @if (!!$data['shiftID'])
              <span>Shift {{ $data['shiftID'] }} - </span>
            @endif
            <span>[ {{ $data['dateRange'][0] }}</span>
            @if ($data['dateRange'][0] !== $data['dateRange'][1])
              <span>- {{ $data['dateRange'][1] }}</span>
            @endif ]
          </div>
          @if ($data['printTitle'] === 'Laporan Dana')
            @include('print.print-reports', [
                'mainData' => $data['mainData'],
                'dateRangeStart' => $data['dateRange'][0],
                'dateRangeEnd' => $data['dateRange'][1],
                'shiftID' => $data['shiftID'],
                'total' => $data['optionalData']['total'],
                'timeNow' => $data['timeNow'],
                'optionalData' => $data['optionalData'],
                'searchOptions' => $data['searchOptions'],
                'verifier' => $data['verifier'],
                'validator' => $data['validator'],
            ])
          @elseif ($data['printTitle'] === 'Laporan Pump Test')
            @include('print.print-pumptests', [
                'mainData' => $data['mainData'],
                'dateRangeStart' => $data['dateRange'][0],
                'dateRangeEnd' => $data['dateRange'][1],
                'shiftID' => $data['shiftID'],
                'total' => $data['optionalData']['total'],
                'timeNow' => $data['timeNow'],
                'optionalData' => $data['optionalData'],
                'verifier' => $data['verifier'],
                'validator' => $data['validator'],
            ])
          @elseif ($data['printTitle'] === 'Laporan Bongkaran')
            @include('print.print-tankdeliveries', [
                'mainData' => $data['mainData'],
                'dateRangeStart' => $data['dateRange'][0],
                'dateRangeEnd' => $data['dateRange'][1],
                'shiftID' => $data['shiftID'],
                'total' => $data['optionalData']['group_by_fuel']['total'],
                'timeNow' => $data['timeNow'],
                'optionalData' => $data['optionalData'],
                'verifier' => $data['verifier'],
                'validator' => $data['validator'],
            ])
          @elseif ($data['printTitle'] === 'Laporan Settlement')
            @include('print.print-sales', [
                'mainData' => $data['mainData'],
                'dateRangeStart' => $data['dateRange'][0],
                'dateRangeEnd' => $data['dateRange'][1],
                'shiftID' => $data['shiftID'],
                'total' => $data['optionalData']['total'],
                'timeNow' => $data['timeNow'],
                'optionalData' => $data['optionalData']['opts'],
                'searchOptions' => $data['searchOptions'],
                'verifier' => $data['verifier'],
                'validator' => $data['validator'],
            ])
          @elseif ($data['printTitle'] === 'Oil Movement Report')
            @include('print.print-opnames', [
                'mainData' => $data['mainData'],
                'dateRangeStart' => $data['dateRange'][0],
                'dateRangeEnd' => $data['dateRange'][1],
                'shiftID' => $data['shiftID'],
                'total' => $data['optionalData']['total'],
                'timeNow' => $data['timeNow'],
                'optionalData' => $data['optionalData'],
                'searchOptions' => $data['searchOptions'],
                'verifier' => $data['verifier'],
                'validator' => $data['validator'],
            ])
          @elseif ($data['printTitle'] === 'Stock Opname')
            @include('print.print-stock-opnames', [
                'mainData' => $data['mainData'],
                'dateRangeStart' => $data['dateRange'][0],
                'dateRangeEnd' => $data['dateRange'][1],
                'shiftID' => $data['shiftID'],
                'total' => $data['optionalData']['total'],
                'timeNow' => $data['timeNow'],
                'optionalData' => $data['optionalData'],
                'searchOptions' => $data['searchOptions'],
            ])
          @elseif ($data['printTitle'] === 'Laporan Laba Rugi')
            @include('print.print-profitlosses', [
                'mainData' => $data['mainData'],
                'total' => $data['total'],
            ])
          @endif
          <script type="text/php">
          use Carbon\Carbon;
          $nowTime = Carbon::parse(now()->toDateTimeString())->format('d/m/Y H:i');
          <?php
          if (in_array($data['printTitle'], $data['portraitTitles'])) {
          ?>
            $pdf->page_text(15, 815, 'Printed : ' . $nowTime, '', 6, array(0, 0, 0));
            $pdf->page_text(275, 815, "Generated by E-SPBU", '', 6, array(0, 0, 0));
            $pdf->page_text(550, 815, "Page: {PAGE_NUM} of {PAGE_COUNT}", '', 6, array(0, 0, 0));
          <?php
          } else {
          ?>
            if (isset($pdf)) {
              $pdf->page_text(15, 570, 'Printed : ' . $nowTime, '', 6, array(0, 0, 0));
              $pdf->page_text(395, 570, "Generated by E-SPBU", '', 6, array(0, 0, 0));
              $pdf->page_text(790, 570, "Page: {PAGE_NUM} of {PAGE_COUNT}", '', 6, array(0, 0, 0));
            }
          <?php
          }
          ?>
        </script>
        </div>
      </div>
    </div>
  </div>
@endsection
@extends('layouts.app')
@section('content')
  <div style="font-family: ui-serif, Georgia, Cambria, 'Times New Roman', 'Times, serif' !important">
    <div class="flex flex-col w-full font-serif">
      <div>
        <div class="flex flex-col w-full h-max" id="printable-area">
          @if (!!$data['kopSurat'])
            @include('print.print-kop-surat', ['kopSurat' => $data['kopSurat']])
          @else
            <div class="font-sans tracking-tight font-bold uppercase text-sm">{{ $data['bussinessName'] }}</div>
            <div class="font-sans tracking-tight font-bold uppercase text-sm">{{ $data['bussinessAddress'] }}</div>
          @endif
          <div class="font-semibold font-sans uppercase text-center mt-1 text-xl"
            style="letter-spacing: 0.04em !important;">
            {{ $data['printTitle'] }}
          </div>
          <div class="font-bold tracking-tight text-base">Period :
            @if (!!$data['shiftID'])
              <span>Shift {{ $data['shiftID'] }} - </span>
            @endif
            <span>[ {{ $data['dateRange'][0] }}</span>
            @if ($data['dateRange'][0] !== $data['dateRange'][1])
              <span>- {{ $data['dateRange'][1] }}</span>
            @endif ]
          </div>
          @if ($data['printTitle'] === 'Laporan Dana')
            @include('print.print-reports', [
                'mainData' => $data['mainData'],
                'dateRangeStart' => $data['dateRange'][0],
                'dateRangeEnd' => $data['dateRange'][1],
                'shiftID' => $data['shiftID'],
                'total' => $data['optionalData']['total'],
                'timeNow' => $data['timeNow'],
                'optionalData' => $data['optionalData'],
                'searchOptions' => $data['searchOptions'],
                'verifier' => $data['verifier'],
                'validator' => $data['validator'],
            ])
          @elseif ($data['printTitle'] === 'Laporan Pump Test')
            @include('print.print-pumptests', [
                'mainData' => $data['mainData'],
                'dateRangeStart' => $data['dateRange'][0],
                'dateRangeEnd' => $data['dateRange'][1],
                'shiftID' => $data['shiftID'],
                'total' => $data['optionalData']['total'],
                'timeNow' => $data['timeNow'],
                'optionalData' => $data['optionalData'],
                'verifier' => $data['verifier'],
                'validator' => $data['validator'],
            ])
          @elseif ($data['printTitle'] === 'Laporan Bongkaran')
            @include('print.print-tankdeliveries', [
                'mainData' => $data['mainData'],
                'dateRangeStart' => $data['dateRange'][0],
                'dateRangeEnd' => $data['dateRange'][1],
                'shiftID' => $data['shiftID'],
                'total' => $data['optionalData']['group_by_fuel']['total'],
                'timeNow' => $data['timeNow'],
                'optionalData' => $data['optionalData'],
                'verifier' => $data['verifier'],
                'validator' => $data['validator'],
            ])
          @elseif ($data['printTitle'] === 'Laporan Settlement')
            @include('print.print-sales', [
                'mainData' => $data['mainData'],
                'dateRangeStart' => $data['dateRange'][0],
                'dateRangeEnd' => $data['dateRange'][1],
                'shiftID' => $data['shiftID'],
                'total' => $data['optionalData']['total'],
                'timeNow' => $data['timeNow'],
                'optionalData' => $data['optionalData']['opts'],
                'searchOptions' => $data['searchOptions'],
                'verifier' => $data['verifier'],
                'validator' => $data['validator'],
            ])
          @elseif ($data['printTitle'] === 'Oil Movement Report')
            @include('print.print-opnames', [
                'mainData' => $data['mainData'],
                'dateRangeStart' => $data['dateRange'][0],
                'dateRangeEnd' => $data['dateRange'][1],
                'shiftID' => $data['shiftID'],
                'total' => $data['optionalData']['total'],
                'timeNow' => $data['timeNow'],
                'optionalData' => $data['optionalData'],
                'searchOptions' => $data['searchOptions'],
                'verifier' => $data['verifier'],
                'validator' => $data['validator'],
            ])
          @elseif ($data['printTitle'] === 'Stock Opname')
            @include('print.print-stock-opnames', [
                'mainData' => $data['mainData'],
                'dateRangeStart' => $data['dateRange'][0],
                'dateRangeEnd' => $data['dateRange'][1],
                'shiftID' => $data['shiftID'],
                'total' => $data['optionalData']['total'],
                'timeNow' => $data['timeNow'],
                'optionalData' => $data['optionalData'],
                'searchOptions' => $data['searchOptions'],
            ])
          @elseif ($data['printTitle'] === 'Laporan Laba Rugi')
            @include('print.print-profitlosses', [
                'mainData' => $data['mainData'],
                'total' => $data['total'],
            ])
          @endif
          <script type="text/php">
          use Carbon\Carbon;
          $nowTime = Carbon::parse(now()->toDateTimeString())->format('d/m/Y H:i');
          <?php
          if (in_array($data['printTitle'], $data['portraitTitles'])) {
          ?>
            $pdf->page_text(15, 815, 'Printed : ' . $nowTime, '', 6, array(0, 0, 0));
            $pdf->page_text(275, 815, "Generated by E-SPBU", '', 6, array(0, 0, 0));
            $pdf->page_text(550, 815, "Page: {PAGE_NUM} of {PAGE_COUNT}", '', 6, array(0, 0, 0));
          <?php
          } else {
          ?>
            if (isset($pdf)) {
              $pdf->page_text(15, 570, 'Printed : ' . $nowTime, '', 6, array(0, 0, 0));
              $pdf->page_text(395, 570, "Generated by E-SPBU", '', 6, array(0, 0, 0));
              $pdf->page_text(790, 570, "Page: {PAGE_NUM} of {PAGE_COUNT}", '', 6, array(0, 0, 0));
            }
          <?php
          }
          ?>
        </script>
        </div>
      </div>
    </div>
  </div>
@endsection
blade
<div>
  <table id="tableprint" class="font-sans table-auto w-full border-collapse" style="border: none; padding: 0;">
    <thead>
      <tr
        style="
        border-top: 1px solid #111827;
        border-bottom: 1px solid #111827;
        border-left: none;
        border-right: none;">
        <th class="text-base whitespace-nowrap text-center">
          #
        </th>
        <th class="text-base whitespace-nowrap text-left">
          Pump
        </th>
        <th class="text-base whitespace-nowrap text-left">
          Hose
        </th>
        <th class="text-base whitespace-nowrap text-left">
          Produk
        </th>
        <th class="text-base whitespace-nowrap text-left">
          Uji
        </th>
        <th class="text-base whitespace-nowrap text-right">
          Harga (Rp.)
        </th>
        <th class="text-base whitespace-nowrap text-right">
          Volume (Rp.)
        </th>
        <th class="text-base whitespace-nowrap text-right">
          Nilai (Rp.)
        </th>
        <th class="text-base whitespace-nowrap text-right">
          Selesai
        </th>
      </tr>
    </thead>
    <tbody>
      @foreach ($mainData as $report)
        <tr class="!border-0" style="border: none;">
          <td class="whitespace-nowrap text-base text-gray-900 !border-0 text-center">
            {{ $report['row_number'] }}
          </td>
          <td class="whitespace-nowrap text-base text-gray-900 !border-0">
            {{ $report['machine'] }}
          </td>
          <td class="whitespace-nowrap text-base text-gray-900 !border-0">
            {{ $report['hose'] }}
          </td>
          <td class="whitespace-nowrap text-base text-gray-900 !border-0">
            {{ $report['fuel'] }}
          </td>
          <td class="whitespace-nowrap text-base text-gray-900 !border-0">
            {{ $report['type'] }}
          </td>
          <td class="whitespace-nowrap text-base text-gray-900 !border-0 text-right">
            {{ number_format($report['fuel_price'], 0, '.', ',') }}
          </td>
          <td class="whitespace-nowrap text-base text-gray-900 !border-0 text-right">
            {{ $report['volume'] }}
          </td>
          <td class="whitespace-nowrap text-base text-gray-900 !border-0 text-right">
            {{ number_format($report['fuel_value'], 0, '.', ',') }}
          </td>
          <td class="whitespace-nowrap text-base text-gray-900 !border-0 text-right">
            {{ $report['cleared_at'] }}
          </td>
        </tr>
    </tbody>
    @endforeach
    <tr class="text-base tableprint-tfoot"
      style="
    border-top: 1px solid black;
    border-bottom: 1px solid black;
    border-left: none;
    border-right: none;
    ">
      <td class="py-1 font-bold text-right">
      </td>
      <td class="py-1 font-bold text-right">
      </td>
      <td class="py-1 font-bold text-right">
      </td>
      <td class="py-1 font-bold text-right">
      </td>
      <td class="py-1 font-bold text-right">
      </td>
      <td class="py-1 font-bold text-right">
        TOTAL
      </td>
      <td class="py-1 font-bold text-right">
        {{ number_format($total['volume'], 2, '.', ',') }}
      </td>
      <td class="py-1 font-bold text-right">
        {{ number_format($total['fuel_value'], 0, '.', ',') }}
      </td>
      <td class="py-1 font-bold text-right">
      </td>
    </tr>
  </table>

  <table id="half-tableprint" class="mt-4" style="border: none; padding: 0;">
    <thead>
      <tr
        style="
        border-top: 1px solid black;
        border-bottom: 1px solid black;
        border-left: none;
        border-right: none;">
        <th class="text-base whitespace-nowrap text-left">
          Jenis BBM
        </th>
        <th class="text-base whitespace-nowrap text-right">
          Volume (L)
        </th>
        <th class="text-base whitespace-nowrap text-right">
          Nilai (Rp.)
        </th>
      </tr>
    </thead>
    <tbody>
      @foreach ($optionalData['group_by_fuel']['data'] as $fuel)
        <tr style="border: none; padding: 0;">
          <td class="whitespace-nowrap text-base text-gray-900">{{ $fuel->fuel }}
          </td>
          <td class="whitespace-nowrap text-base text-gray-900 text-right">
            {{ number_format((float) $fuel->volume, 2, '.', ',') }}
          </td>
          <td class="whitespace-nowrap text-base text-gray-900 text-right">
            {{ number_format((float) $fuel->fuel_value, 0, '.', ',') }}
          </td>
        </tr>
      @endforeach
    </tbody>
    <tfoot>
      <tr
        style="
        border-top: 1px solid black;
        border-bottom: 1px solid black;
        border-left: none;
        border-right: none;">
        <th class="text-base whitespace-nowrap text-right">
          TOTAL
        </th>
        <th class="text-base whitespace-nowrap text-right">
          {{ number_format($total['volume'], 2, '.', ',') }}
        </th>
        <th class="text-base whitespace-nowrap text-right">
          {{ number_format($total['fuel_value'], 0, '.', ',') }}
        </th>
      </tr>
    </tfoot>
  </table>

  @include('print.print-signature', ['verifier' => $verifier])
</div>
<div>
  <table id="tableprint" class="font-sans table-auto w-full border-collapse" style="border: none; padding: 0;">
    <thead>
      <tr
        style="
        border-top: 1px solid #111827;
        border-bottom: 1px solid #111827;
        border-left: none;
        border-right: none;">
        <th class="text-base whitespace-nowrap text-center">
          #
        </th>
        <th class="text-base whitespace-nowrap text-left">
          Pump
        </th>
        <th class="text-base whitespace-nowrap text-left">
          Hose
        </th>
        <th class="text-base whitespace-nowrap text-left">
          Produk
        </th>
        <th class="text-base whitespace-nowrap text-left">
          Uji
        </th>
        <th class="text-base whitespace-nowrap text-right">
          Harga (Rp.)
        </th>
        <th class="text-base whitespace-nowrap text-right">
          Volume (Rp.)
        </th>
        <th class="text-base whitespace-nowrap text-right">
          Nilai (Rp.)
        </th>
        <th class="text-base whitespace-nowrap text-right">
          Selesai
        </th>
      </tr>
    </thead>
    <tbody>
      @foreach ($mainData as $report)
        <tr class="!border-0" style="border: none;">
          <td class="whitespace-nowrap text-base text-gray-900 !border-0 text-center">
            {{ $report['row_number'] }}
          </td>
          <td class="whitespace-nowrap text-base text-gray-900 !border-0">
            {{ $report['machine'] }}
          </td>
          <td class="whitespace-nowrap text-base text-gray-900 !border-0">
            {{ $report['hose'] }}
          </td>
          <td class="whitespace-nowrap text-base text-gray-900 !border-0">
            {{ $report['fuel'] }}
          </td>
          <td class="whitespace-nowrap text-base text-gray-900 !border-0">
            {{ $report['type'] }}
          </td>
          <td class="whitespace-nowrap text-base text-gray-900 !border-0 text-right">
            {{ number_format($report['fuel_price'], 0, '.', ',') }}
          </td>
          <td class="whitespace-nowrap text-base text-gray-900 !border-0 text-right">
            {{ $report['volume'] }}
          </td>
          <td class="whitespace-nowrap text-base text-gray-900 !border-0 text-right">
            {{ number_format($report['fuel_value'], 0, '.', ',') }}
          </td>
          <td class="whitespace-nowrap text-base text-gray-900 !border-0 text-right">
            {{ $report['cleared_at'] }}
          </td>
        </tr>
    </tbody>
    @endforeach
    <tr class="text-base tableprint-tfoot"
      style="
    border-top: 1px solid black;
    border-bottom: 1px solid black;
    border-left: none;
    border-right: none;
    ">
      <td class="py-1 font-bold text-right">
      </td>
      <td class="py-1 font-bold text-right">
      </td>
      <td class="py-1 font-bold text-right">
      </td>
      <td class="py-1 font-bold text-right">
      </td>
      <td class="py-1 font-bold text-right">
      </td>
      <td class="py-1 font-bold text-right">
        TOTAL
      </td>
      <td class="py-1 font-bold text-right">
        {{ number_format($total['volume'], 2, '.', ',') }}
      </td>
      <td class="py-1 font-bold text-right">
        {{ number_format($total['fuel_value'], 0, '.', ',') }}
      </td>
      <td class="py-1 font-bold text-right">
      </td>
    </tr>
  </table>

  <table id="half-tableprint" class="mt-4" style="border: none; padding: 0;">
    <thead>
      <tr
        style="
        border-top: 1px solid black;
        border-bottom: 1px solid black;
        border-left: none;
        border-right: none;">
        <th class="text-base whitespace-nowrap text-left">
          Jenis BBM
        </th>
        <th class="text-base whitespace-nowrap text-right">
          Volume (L)
        </th>
        <th class="text-base whitespace-nowrap text-right">
          Nilai (Rp.)
        </th>
      </tr>
    </thead>
    <tbody>
      @foreach ($optionalData['group_by_fuel']['data'] as $fuel)
        <tr style="border: none; padding: 0;">
          <td class="whitespace-nowrap text-base text-gray-900">{{ $fuel->fuel }}
          </td>
          <td class="whitespace-nowrap text-base text-gray-900 text-right">
            {{ number_format((float) $fuel->volume, 2, '.', ',') }}
          </td>
          <td class="whitespace-nowrap text-base text-gray-900 text-right">
            {{ number_format((float) $fuel->fuel_value, 0, '.', ',') }}
          </td>
        </tr>
      @endforeach
    </tbody>
    <tfoot>
      <tr
        style="
        border-top: 1px solid black;
        border-bottom: 1px solid black;
        border-left: none;
        border-right: none;">
        <th class="text-base whitespace-nowrap text-right">
          TOTAL
        </th>
        <th class="text-base whitespace-nowrap text-right">
          {{ number_format($total['volume'], 2, '.', ',') }}
        </th>
        <th class="text-base whitespace-nowrap text-right">
          {{ number_format($total['fuel_value'], 0, '.', ',') }}
        </th>
      </tr>
    </tfoot>
  </table>

  @include('print.print-signature', ['verifier' => $verifier])
</div>

Generate PDF Frontend Implementation

  • Create a composable function to generate the PDF file, named exportPDF in print.js
print.js
js
export default function usePrints() {
  const prints = ref({})
  const isLoadingPrint = ref(false);
  const dateYMDFormat = "YYYY-MM-DD"

  const exportPDF = async (
    requests,
    periode_start_at,
    periode_end_at,
    title,
    shift = null
  ) => {
    isLoadingPrint.value = true
    fetch('/api/export/pdf?requests=' + JSON.stringify(requests))
      .then(response => response.blob())
      .then(blob => {
        let startAt = moment(periode_start_at).format(dateYMDFormat)
        let endAt = moment(periode_end_at).format(dateYMDFormat)

        let date = `${startAt}to${endAt}`
        if (startAt === endAt) {
          date = startAt
        }

        let filename = `${date}` + `_` + `${title}.pdf`
        if (shift) {
          filename = `${date}_` + `S` + `${shift}_${title}.pdf`
        }
        // Create a temporary URL for the blob
        const url = window.URL.createObjectURL(blob);
        const link = document.createElement('a');
        link.href = url;
        link.setAttribute('download', `${filename}`);
        document.body.appendChild(link);

        // Trigger the click event to download the file
        link.click();

        // Clean up the temporary link element
        link.parentNode.removeChild(link);
        isLoadingPrint.value = false
      })
      .catch(error => {
        isLoadingPrint.value = false;
      })
      .finally(() => {
        isLoadingPrint.value = false
      });
  }

  return {
    prints, exportPDF, isLoadingPrint
  }
}
export default function usePrints() {
  const prints = ref({})
  const isLoadingPrint = ref(false);
  const dateYMDFormat = "YYYY-MM-DD"

  const exportPDF = async (
    requests,
    periode_start_at,
    periode_end_at,
    title,
    shift = null
  ) => {
    isLoadingPrint.value = true
    fetch('/api/export/pdf?requests=' + JSON.stringify(requests))
      .then(response => response.blob())
      .then(blob => {
        let startAt = moment(periode_start_at).format(dateYMDFormat)
        let endAt = moment(periode_end_at).format(dateYMDFormat)

        let date = `${startAt}to${endAt}`
        if (startAt === endAt) {
          date = startAt
        }

        let filename = `${date}` + `_` + `${title}.pdf`
        if (shift) {
          filename = `${date}_` + `S` + `${shift}_${title}.pdf`
        }
        // Create a temporary URL for the blob
        const url = window.URL.createObjectURL(blob);
        const link = document.createElement('a');
        link.href = url;
        link.setAttribute('download', `${filename}`);
        document.body.appendChild(link);

        // Trigger the click event to download the file
        link.click();

        // Clean up the temporary link element
        link.parentNode.removeChild(link);
        isLoadingPrint.value = false
      })
      .catch(error => {
        isLoadingPrint.value = false;
      })
      .finally(() => {
        isLoadingPrint.value = false
      });
  }

  return {
    prints, exportPDF, isLoadingPrint
  }
}
  • Import the exportPDF function to the page component
.vue
vue
<script setup>
import usePrints from "../../composables/prints";

// other code..
const { prints, exportPDF, isLoadingPrint } = usePrints();

</script>
<script setup>
import usePrints from "../../composables/prints";

// other code..
const { prints, exportPDF, isLoadingPrint } = usePrints();

</script>
  • Create a button and the on click function to generate the PDF file
.vue
vue
<template>
  <!-- .. -->
    <div v-if="!isLoadingPumpTests && pumpTests.data.length > 0
      " class="flex flex-row gap-2">
      <button v-if="isLoadingPrint"
        class="opacity-75 cursor-not-allowed mt-4 mb-2 flex w-max flex-row justify-center rounded-lg border border-rose-500 bg-rose-100 p-2 text-rose-400 hover:bg-rose-200 transition ease-in-out items-center">
        <div
          class="inline-block animate-spin w-4 h-4 mr-2 border-t-2 border-t-white border-r-2 border-r-white border-b-2 border-b-white border-l-rose-500 border-l-2 rounded-full">
        </div>
        <span class="ml-1 text-sm font-extrabold">Export PDF</span>
      </button>
      <button v-else v-on:click="onExportPDF()"
        class="mt-4 mb-2 flex w-max flex-row justify-center rounded-lg border border-rose-500 bg-rose-100 p-2 text-rose-500 hover:bg-rose-200 transition ease-in-out items-center">
        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
          <path fill="currentColor" fill-rule="evenodd"
            d="M14 4.5V14a2 2 0 0 1-2 2h-1v-1h1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5L14 4.5ZM1.6 11.85H0v3.999h.791v-1.342h.803c.287 0 .531-.057.732-.173c.203-.117.358-.275.463-.474a1.42 1.42 0 0 0 .161-.677c0-.25-.053-.476-.158-.677a1.176 1.176 0 0 0-.46-.477c-.2-.12-.443-.179-.732-.179Zm.545 1.333a.795.795 0 0 1-.085.38a.574.574 0 0 1-.238.241a.794.794 0 0 1-.375.082H.788V12.48h.66c.218 0 .389.06.512.181c.123.122.185.296.185.522Zm1.217-1.333v3.999h1.46c.401 0 .734-.08.998-.237a1.45 1.45 0 0 0 .595-.689c.13-.3.196-.662.196-1.084c0-.42-.065-.778-.196-1.075a1.426 1.426 0 0 0-.589-.68c-.264-.156-.599-.234-1.005-.234H3.362Zm.791.645h.563c.248 0 .45.05.609.152a.89.89 0 0 1 .354.454c.079.201.118.452.118.753a2.3 2.3 0 0 1-.068.592a1.14 1.14 0 0 1-.196.422a.8.8 0 0 1-.334.252a1.298 1.298 0 0 1-.483.082h-.563v-2.707Zm3.743 1.763v1.591h-.79V11.85h2.548v.653H7.896v1.117h1.606v.638H7.896Z" />
        </svg>
        <span class="ml-1 text-sm font-extrabold">Export PDF</span>
      </button>

    </div>
  <!-- .. -->
</template>

<script setup>
// ..

function onExportPDF() {
  let print_title = "Laporan Pump Test";
  let request = {
    print_title: print_title,
    shift_id: search_shift.value,
    date_range_start: search_cleared_at.value[0],
    date_range_end: search_cleared_at.value[1],
    param: JSON.stringify({
      page: pageNumber.value,
      search_machine: search_machine.value,
      search_shift: search_shift.value,
      search_cleared_start_at: search_cleared_at.value[0],
      search_cleared_end_at: search_cleared_at.value[1],
      is_full_page: is_full_page.value,
      search_per_page: search_per_page.value,
      order_column: order_column.value,
      order_direction: order_direction.value,
    }),
  };

  exportPDF(request, search_cleared_at.value[0], search_cleared_at.value[1], print_title, search_shift.value);
}
</script>
<template>
  <!-- .. -->
    <div v-if="!isLoadingPumpTests && pumpTests.data.length > 0
      " class="flex flex-row gap-2">
      <button v-if="isLoadingPrint"
        class="opacity-75 cursor-not-allowed mt-4 mb-2 flex w-max flex-row justify-center rounded-lg border border-rose-500 bg-rose-100 p-2 text-rose-400 hover:bg-rose-200 transition ease-in-out items-center">
        <div
          class="inline-block animate-spin w-4 h-4 mr-2 border-t-2 border-t-white border-r-2 border-r-white border-b-2 border-b-white border-l-rose-500 border-l-2 rounded-full">
        </div>
        <span class="ml-1 text-sm font-extrabold">Export PDF</span>
      </button>
      <button v-else v-on:click="onExportPDF()"
        class="mt-4 mb-2 flex w-max flex-row justify-center rounded-lg border border-rose-500 bg-rose-100 p-2 text-rose-500 hover:bg-rose-200 transition ease-in-out items-center">
        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
          <path fill="currentColor" fill-rule="evenodd"
            d="M14 4.5V14a2 2 0 0 1-2 2h-1v-1h1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5L14 4.5ZM1.6 11.85H0v3.999h.791v-1.342h.803c.287 0 .531-.057.732-.173c.203-.117.358-.275.463-.474a1.42 1.42 0 0 0 .161-.677c0-.25-.053-.476-.158-.677a1.176 1.176 0 0 0-.46-.477c-.2-.12-.443-.179-.732-.179Zm.545 1.333a.795.795 0 0 1-.085.38a.574.574 0 0 1-.238.241a.794.794 0 0 1-.375.082H.788V12.48h.66c.218 0 .389.06.512.181c.123.122.185.296.185.522Zm1.217-1.333v3.999h1.46c.401 0 .734-.08.998-.237a1.45 1.45 0 0 0 .595-.689c.13-.3.196-.662.196-1.084c0-.42-.065-.778-.196-1.075a1.426 1.426 0 0 0-.589-.68c-.264-.156-.599-.234-1.005-.234H3.362Zm.791.645h.563c.248 0 .45.05.609.152a.89.89 0 0 1 .354.454c.079.201.118.452.118.753a2.3 2.3 0 0 1-.068.592a1.14 1.14 0 0 1-.196.422a.8.8 0 0 1-.334.252a1.298 1.298 0 0 1-.483.082h-.563v-2.707Zm3.743 1.763v1.591h-.79V11.85h2.548v.653H7.896v1.117h1.606v.638H7.896Z" />
        </svg>
        <span class="ml-1 text-sm font-extrabold">Export PDF</span>
      </button>

    </div>
  <!-- .. -->
</template>

<script setup>
// ..

function onExportPDF() {
  let print_title = "Laporan Pump Test";
  let request = {
    print_title: print_title,
    shift_id: search_shift.value,
    date_range_start: search_cleared_at.value[0],
    date_range_end: search_cleared_at.value[1],
    param: JSON.stringify({
      page: pageNumber.value,
      search_machine: search_machine.value,
      search_shift: search_shift.value,
      search_cleared_start_at: search_cleared_at.value[0],
      search_cleared_end_at: search_cleared_at.value[1],
      is_full_page: is_full_page.value,
      search_per_page: search_per_page.value,
      order_column: order_column.value,
      order_direction: order_direction.value,
    }),
  };

  exportPDF(request, search_cleared_at.value[0], search_cleared_at.value[1], print_title, search_shift.value);
}
</script>

Preview

Released under the MIT License.