Add pdf export.

master
Lio Novelli 2021-09-21 01:13:15 +02:00
parent 9cf4d23012
commit e605729aaf
9 changed files with 313 additions and 15 deletions

View File

@ -69,5 +69,5 @@
*** Get csv file *** Get csv file
curl 'https://drunomics.myjetbrains.com/youtrack/api/reports/83-554/export/csv?&$top=-1' -H 'Accept: application/json, text/plain, */*' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H "Authorization: Bearer $TKN" > ~/Documents/Drunomics/workhours/2021/21-09.csv ~curl 'https://drunomics.myjetbrains.com/youtrack/api/reports/83-554/export/csv?&$top=-1' -H 'Accept: application/json, text/plain, */*' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H "Authorization: Bearer $TKN" > ~/Documents/Drunomics/workhours/2021/21-09.csv~

View File

@ -0,0 +1,59 @@
<html>
<head>
<style>
body {
font-family: sans-serif;
font-size: 10pt;
}
p { margin: 0pt; }
td { vertical-align: top; }
.items td {
border-left: 0.1mm solid #000000;
border-right: 0.1mm solid #000000;
}
table thead td { background-color: #EEEEEE;
text-align: center;
border: 0.1mm solid #000000;
font-variant: small-caps;
}
.items td.blanktotal {
background-color: #EEEEEE;
border: 0.1mm solid #000000;
background-color: #FFFFFF;
border: 0mm none #000000;
border-top: 0.1mm solid #000000;
border-right: 0.1mm solid #000000;
}
.items td.totals {
text-align: right;
border: 0.1mm solid #000000;
}
.items td.cost,
.center {
text-align: center;
}
.right {
text-align: right;
}
</style>
</head>
<body>
<p class="center">My company name</br>My company address</br>123456789, mycompany@email.com</br>987654321</p>
<p></p>
<p>Other company</br>Other company address</br>1111 City</br>State</p>
<p></p>
<p></p>
<p></p>
<p class="right">City, on 20. 9. 2021</p>
<p><strong>Honorarnote</br>Nummer [[number]]</strong></p>
<p></p>
<p>Für meine Tätigkeit Programmieren von [[date_start]] bis [[date_end]] erlaube ich mir, folgenden Betrag in Rechnung zu stellen:</p>
[[table]]
<p>Ich ersuche Sie höflich, den oben angeführt auf meine Kontonummer [[account_number]] mit der Bankleitzahl ABCDSI33 zu überweisen.</p>
<p>Vielen Dank für den Auftrag,</br>
mit besten Grüßen,</p>
<p></p>
<p></p>
<p>My name</p>
</body>
</html>

View File

@ -1,9 +1,22 @@
########################################## ##########################################
# Configuration file for RprtCli Command # # Configuration file for RprtCli Command #
########################################## ##########################################
tracking service: tracking_service:
youtrack: youtrack:
auth token: '<value from youtrack hub>' auth_token: '<value from youtrack hub>'
base_url: 'https://test.youtrack.com'
report_id: '<89-123>'
export:
template_path: '~/.config/rprt-cli/invoice-template.html'
output: '/tmp/YYYY-mm-invoice.pdf'
tokens:
key: 'value to replace key'
another_key: 'value to replace another key'
labels:
- 'Project'
- 'Hours'
- 'Rate'
- 'Price'
# reports: # reports:
# report short name: # report short name:
# table: # table:
@ -21,10 +34,5 @@ projects:
time column: 4 time column: 4
# time format m - minutes, h - hours # time format m - minutes, h - hours
time format: 'm' time format: 'm'
labels:
project: Project
# hours: Quantity
hours: Hours
rate: 'Price per hour'
price: Price
locale: 'en_GB' locale: 'en_GB'

View File

@ -9,6 +9,9 @@ use RprtCli\Utils\Configuration\ConfigurationService;
use RprtCli\Utils\CsvReport\CsvReport; use RprtCli\Utils\CsvReport\CsvReport;
use RprtCli\Utils\CsvReport\CsvReportInterface; use RprtCli\Utils\CsvReport\CsvReportInterface;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use Mpdf\Mpdf;
use RprtCli\Utils\PdfExport\PdfExportInterface;
use RprtCli\Utils\PdfExport\PdfExportService;
use RprtCli\Utils\TimeTrackingServices\YoutrackInterface; use RprtCli\Utils\TimeTrackingServices\YoutrackInterface;
use RprtCli\Utils\TimeTrackingServices\YoutrackService; use RprtCli\Utils\TimeTrackingServices\YoutrackService;
@ -22,6 +25,7 @@ return [
// 'translator' => ['default_path' => '%kernel.project_dir%/translations'], // 'translator' => ['default_path' => '%kernel.project_dir%/translations'],
// 'guzzle' => create()->constructor(Client::class), // 'guzzle' => create()->constructor(Client::class),
'guzzle' => get(Client::class), 'guzzle' => get(Client::class),
'mpdf' => get(Mpdf::class),
ConfigurationInterface::class => get(ConfigurationService::class), ConfigurationInterface::class => get(ConfigurationService::class),
ConfigurationService::class => create()->constructor( ConfigurationService::class => create()->constructor(
get('config.path'), get('config.path'),
@ -34,6 +38,12 @@ return [
get('guzzle') get('guzzle')
), ),
'youtrack.service' => get(YoutrackInterface::class), 'youtrack.service' => get(YoutrackInterface::class),
PdfExportInterface::class => get(PdfExportService::class),
PdfExportService::class => create()->constructor(
get('config.service'),
get('mpdf')
),
'pdf_export.service' => get(PdfExportInterface::class),
// 'locale' => get('config.service')->method('get', 'en'), // 'locale' => get('config.service')->method('get', 'en'),
// Translator::class => create()->constructor('sl')->method('addLoader', 'po', new PoFileLoader), // Translator::class => create()->constructor('sl')->method('addLoader', 'po', new PoFileLoader),
// 'translator' => get(Translator::class), // 'translator' => get(Translator::class),
@ -45,6 +55,7 @@ return [
RprtCommand::class => create()->constructor( RprtCommand::class => create()->constructor(
get('csv.report'), get('csv.report'),
get('config.service'), get('config.service'),
get('youtrack.service') get('youtrack.service'),
get('pdf_export.service')
), ),
]; ];

View File

@ -8,6 +8,7 @@ namespace RprtCli\Commands;
use RprtCli\Utils\Configuration\ConfigurationInterface; use RprtCli\Utils\Configuration\ConfigurationInterface;
use RprtCli\Utils\CsvReport\CsvReportInterface; use RprtCli\Utils\CsvReport\CsvReportInterface;
use RprtCli\Utils\PdfExport\PdfExportInterface;
use RprtCli\Utils\TimeTrackingServices\YoutrackInterface; use RprtCli\Utils\TimeTrackingServices\YoutrackInterface;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\Table;
@ -30,15 +31,19 @@ class RprtCommand extends Command
protected $youtrack; protected $youtrack;
protected $pdfExport;
public function __construct( public function __construct(
CsvReportInterface $csv, CsvReportInterface $csv,
ConfigurationInterface $configuration, ConfigurationInterface $configuration,
YoutrackInterface $youtrack, YoutrackInterface $youtrack,
PdfExportInterface $pdf_export,
?string $name = null ?string $name = null
) { ) {
$this->csv = $csv; $this->csv = $csv;
$this->configuration = $configuration; $this->configuration = $configuration;
$this->youtrack = $youtrack; $this->youtrack = $youtrack;
$this->pdfExport = $pdf_export;
parent::__construct($name); parent::__construct($name);
} }
@ -82,10 +87,23 @@ class RprtCommand extends Command
$test = $this->youtrack->testYoutrackapi(); $test = $this->youtrack->testYoutrackapi();
$output->writeln($test); $output->writeln($test);
} }
if ($file = $input->getOption('file')) { if ($youtrack = $input->getOption('youtrack')) {
$report_id = $this->youtrack->getReportId();
$file = $this->youtrack->downloadReport($report_id);
}
if ($youtrack || $file = $input->getOption('file')) {
// Youtrack can also provide a file name.
var_dump($file);
$data = $this->csv->getReportData($file); $data = $this->csv->getReportData($file);
$table = $this->generateTable($output, $data); $table = $this->generateTable($output, $data);
$table->render(); $table->render();
if ($pdf = $input->getOption('pdf')) {
$nice_data = $this->csv->arangeDataForPdfExport($data);
// @TODO method gatherTokens();
$this->pdfExport->fromDataToPdf($nice_data);
}
return Command::SUCCESS; return Command::SUCCESS;
} }

View File

@ -58,9 +58,10 @@ class CsvReport implements CsvReportInterface
/** /**
* Get correct values from the raw data lines of csv. * Get correct values from the raw data lines of csv.
* *
* * @param array $rawData
* Columns with data are specified in config. * Columns with data are specified in config.
* *
* @return array
* Project key and unit of time spent. * Project key and unit of time spent.
*/ */
protected function parseCsvFile(array $rawData) : array protected function parseCsvFile(array $rawData) : array
@ -74,6 +75,41 @@ class CsvReport implements CsvReportInterface
return []; return [];
} }
public function arangeDataForPdfExport(array $data): array {
[$rows, $totalHours, $totalPrice] = [[], 0, 0];
$projectsConfig = $this->configurationService->get('projects');
$header = $this->configurationService->get('export.labels', null);
if (is_array($header)) {
$rows[] = $header;
}
foreach ($projectsConfig as $name => $config) {
if (! isset($data[$name])) {
// @TODO Proper error handling.
var_dump('Project ' . $name . ' is not set!');
continue;
}
$hours = $data[$name];
if ($config['time_format'] === 'm') {
$hours /= 60;
}
$price = $hours * (int) $config['price'];
$row = [
$config['name'],
$hours,
$config['price'],
$hours * $config['price'],
];
$rows[] = $row;
$totalHours += $hours;
$totalPrice += $price;
}
// @TODO Check rate in final result.
// $rows[] = [$this->translator->trans('Sum'), $totalHours, $config['price'], $totalPrice];
$rows[] = ['Sum', $totalHours, $config['price'], $totalPrice];
return $rows;
}
protected function dummyConfig() : array protected function dummyConfig() : array
{ {
return [ return [

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace RprtCli\Utils\PdfExport;
/**
* Handles exporting parsed csv data to pdf files.
*/
interface PdfExportInterface {
/**
* Retrieves path to template file either from command option, configuration
* or from uer input.
*/
public function getTemplatePath(): ?string;
public function setTemplatePath(string $path): void;
/**
* Creates html table from parsed csv data.
*
* @param array $data
* First line is header. The rest of them is table body.
*/
public function parsedDataToHtmlTable(array $data): ?string;
/**
* Reads the template file and replaces token values.
*/
public function replaceTokensInTemplate(string $template_path, array $tokens): ?string;
/**
* Creates and export file.
*
* @param string $html
* Template file with tokens replaced.
*
* @return bool
* True if export was successfull.
*/
public function pdfExport(string $html) : bool;
}

View File

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace RprtCli\Utils\PdfExport;
use Exception;
use RprtCli\Utils\Configuration\ConfigurationInterface;
use Mpdf\Output\Destination;
class PdfExportService implements PdfExportInterface {
protected $templatePath;
protected $output;
protected $config;
protected $mpdf;
public function __construct(ConfigurationInterface $config, $mpdf) {
$this->config = $config;
$this->mpdf = $mpdf;
}
public function getTemplatePath(): ?string
{
if (!isset($this->templatePath)) {
$template_path = $this->config->get('report.template_path', FALSE);
if (!$template_path) {
$template_path = readline('Enter template file path: ');
}
if (!file_exists($template_path)) {
throw new Exception('Template file not found!');
}
$this->templatePath = $template_path;
}
return $this->templatePath;
}
public function setTemplatePath(string $path): void {
if (file_exists($path)) {
$this->templatePath = $path;
return;
}
throw new Exception('Template file not found!');
}
public function parsedDataToHtmlTable(array $data): ?string {
$table = '<table><thead><tr>';
$header = array_shift($data);
foreach ($header as $cell) {
$table .= "<th>{$cell}</th>";
}
$table .= '</tr></thead><tbody>';
foreach ($data as $row) {
$cells = [];
foreach ($row as $cell) {
$cells[] = "<td>{$cell}</td>";
}
$rows[] = '<tr>' . implode($cells) . '</tr>';
}
$table .= implode('', $rows);
$table .= '</tbody></table>';
return $table;
}
public function replaceTokensInTemplate(string $template_path, array $tokens): ?string
{
$template = file_get_contents($template_path);
foreach ($tokens as $key => $value) {
$template = str_replace("[[{$key}]]", $value, $template);
}
return $template;
}
public function pdfExport(string $html, $output = null): bool {
$this->mpdf->SetProtection(array('print'));
$this->mpdf->SetTitle("Acme Trading Co. - Invoice");
$this->mpdf->SetAuthor("Acme Trading Co.");
$this->mpdf->SetDisplayMode('fullpage');
$this->mpdf->WriteHTML($html);
if (!$this->output) {
$this->output = $this->config->get('report.output', NULL) ?? readline('Enter output file path: ');
}
$this->mpdf->Output($this->output, Destination::FILE);
return file_exists($this->output);
}
public function setOutput(string $path) {
$this->output = $path;
}
public function fromDataToPdf(array $data, array $tokens = []): bool {
$template_path = $this->getTemplatePath();
$tokens['table'] = $this->parsedDataToHtmlTable($data);
$html = $this->replaceTokensInTemplate($template_path, $tokens);
$success = $this->pdfExport($html);
return $success;
}
}

View File

@ -12,6 +12,9 @@ use RprtCli\Utils\Configuration\ConfigurationInterface;
class YoutrackService implements YoutrackInterface class YoutrackService implements YoutrackInterface
{ {
protected $ytToken;
protected $ytBaseUrl;
protected $config; protected $config;
protected $httpClient; protected $httpClient;
@ -51,7 +54,7 @@ class YoutrackService implements YoutrackInterface
{ {
// --report option value should take precedence. // --report option value should take precedence.
// @TODO error handling. // @TODO error handling.
$yt_report_id = $this->config->get('youtrack.report_id'); $yt_report_id = $this->config->get('tracking_service.youtrack.report_id');
if (!$yt_report_id) { if (!$yt_report_id) {
$yt_report_id = readline('Enter the report id: '); $yt_report_id = readline('Enter the report id: ');
} }
@ -81,6 +84,9 @@ class YoutrackService implements YoutrackInterface
} }
protected function getYtToken(): string { protected function getYtToken(): string {
if (isset($this->ytToken)) {
return $this->ytToken;
}
$yt_token = $this->config->get('tracking_service.youtrack.auth_token', FALSE); // $yt_token = $this->config->get('tracking_service.youtrack.auth_token', FALSE); //
if (!$yt_token) { if (!$yt_token) {
$yt_token = readline('Enter your youtrack authentication token: '); $yt_token = readline('Enter your youtrack authentication token: ');
@ -88,9 +94,18 @@ class YoutrackService implements YoutrackInterface
return $yt_token; return $yt_token;
} }
public function setYtToken(string $token): void {
$this->ytToken = $token;
}
protected function getYtUrl(string $path = ''): ?string { protected function getYtUrl(string $path = ''): ?string {
if (isset($this->ytBaseUrl)) {
$yt_base_url = $this->ytBaseUrl;
}
else {
$yt_base_url = $this->config->get('tracking_service.youtrack.base_url', FALSE); $yt_base_url = $this->config->get('tracking_service.youtrack.base_url', FALSE);
if (!$yt_base_url) { }
if (empty($yt_base_url)) {
$yt_base_url = readline('Enter base url for of the youtrack service: '); $yt_base_url = readline('Enter base url for of the youtrack service: ');
} }
if (!empty($path)) { if (!empty($path)) {
@ -98,4 +113,8 @@ class YoutrackService implements YoutrackInterface
} }
return $yt_base_url; return $yt_base_url;
} }
public function setYtUrl(string $base_url) {
$this->ytBaseUrl = $base_url;
}
} }