mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-05-29 14:44:55 +00:00
Feat(Gotenberg): Opt-in alternative pdf generation for modern CSS (#184)
* WIP(gotenberg): add pdf generation abstraction and UI * feat(pdf): settings validate(clien+server) & save * fix(gotenberg): Use correct default papersize chore(gotengberg): Remove unused GOTENBERG_MARGINS env from .env * style(gotenberg): fix linter/styling issues * fix(pdf): use pdf config policy * fix: revert accidental capitalization in mail config vue * Update composer, remove whitespace typo * Fix small typos * fix cookie/env issue * Add gotenberg to .dev, move admin menu item up
This commit is contained in:
@@ -63,6 +63,11 @@ services:
|
||||
networks:
|
||||
- invoiceshelf-dev
|
||||
|
||||
pdf:
|
||||
image: gotenberg/gotenberg:8
|
||||
networks:
|
||||
- invoiceshelf-dev
|
||||
|
||||
networks:
|
||||
invoiceshelf-dev:
|
||||
|
||||
|
||||
@@ -62,6 +62,11 @@ services:
|
||||
networks:
|
||||
- invoiceshelf-dev
|
||||
|
||||
pdf:
|
||||
image: gotenberg/gotenberg:8
|
||||
networks:
|
||||
- invoiceshelf-dev
|
||||
|
||||
networks:
|
||||
invoiceshelf-dev:
|
||||
|
||||
|
||||
@@ -50,5 +50,10 @@ services:
|
||||
networks:
|
||||
- invoiceshelf-dev
|
||||
|
||||
pdf:
|
||||
image: gotenberg/gotenberg:8
|
||||
networks:
|
||||
- invoiceshelf-dev
|
||||
|
||||
networks:
|
||||
invoiceshelf-dev:
|
||||
|
||||
@@ -54,3 +54,7 @@ TRUSTED_PROXIES="*"
|
||||
|
||||
CRON_JOB_AUTH_TOKEN=""
|
||||
LOG_STACK=single
|
||||
|
||||
PDF_DRIVER=dompdf
|
||||
GOTENBERG_HOST=
|
||||
GOTENBERG_PAPERSIZE=
|
||||
|
||||
16
app/Facades/PDF.php
Normal file
16
app/Facades/PDF.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
/**
|
||||
* @method static \Psr\Http\Message\ResponseInterface loadView(string $template)
|
||||
*/
|
||||
class PDF extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'pdf.driver';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\PDFConfigurationRequest;
|
||||
use App\Space\EnvironmentManager;
|
||||
|
||||
class PDFConfigurationController extends Controller
|
||||
{
|
||||
/**
|
||||
* @var EnvironmentManager
|
||||
*/
|
||||
protected $environmentManager;
|
||||
|
||||
public function __construct(EnvironmentManager $environmentManager)
|
||||
{
|
||||
$this->environmentManager = $environmentManager;
|
||||
}
|
||||
|
||||
public function getDrivers()
|
||||
{
|
||||
$this->authorize('manage pdf config');
|
||||
|
||||
$drivers = [
|
||||
'dompdf',
|
||||
'gotenberg',
|
||||
];
|
||||
|
||||
return response()->json($drivers);
|
||||
}
|
||||
|
||||
public function getEnvironment()
|
||||
{
|
||||
$this->authorize('manage pdf config');
|
||||
|
||||
$config = [
|
||||
'pdf_driver' => config('pdf.driver'),
|
||||
'gotenberg_host' => config('pdf.gotenberg.host'),
|
||||
'gotenberg_margins' => config('pdf.gotenberg.margins'),
|
||||
'gotenberg_papersize' => config('pdf.gotenberg.papersize'),
|
||||
];
|
||||
|
||||
return response()->json($config);
|
||||
}
|
||||
|
||||
public function saveEnvironment(PDFConfigurationRequest $request)
|
||||
{
|
||||
$this->authorize('manage pdf config');
|
||||
$results = $this->environmentManager->savePDFVariables($request);
|
||||
|
||||
return response()->json($results);
|
||||
}
|
||||
}
|
||||
57
app/Http/Requests/PDFConfigurationRequest.php
Normal file
57
app/Http/Requests/PDFConfigurationRequest.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class PDFConfigurationRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
switch ($this->get('pdf_driver')) {
|
||||
case 'dompdf':
|
||||
return [
|
||||
'pdf_driver' => [
|
||||
'required',
|
||||
'string',
|
||||
],
|
||||
];
|
||||
break;
|
||||
|
||||
case 'gotenberg':
|
||||
return [
|
||||
'pdf_driver' => [
|
||||
'required',
|
||||
'string',
|
||||
],
|
||||
'gotenberg_host' => [
|
||||
'required',
|
||||
'url',
|
||||
],
|
||||
'gotenberg_papersize' => [
|
||||
function ($attribute, $value, $fail) {
|
||||
($attribute); // unused
|
||||
$reg = "/^\d+(pt|px|pc|mm|cm|in) \d+(pt|px|pc|mm|cm|in)$/";
|
||||
if (! preg_match($reg, $value)) {
|
||||
$fail('Invalid papersize, must be in format "210mm 297mm". Accepts: pt,px,pc,mm,cm,in');
|
||||
}
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
break;
|
||||
}
|
||||
throw new \InvalidArgumentException('Invalid PDFDriver requested');
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,9 @@ use App;
|
||||
use App\Mail\SendEstimateMail;
|
||||
use App\Services\SerialNumberFormatter;
|
||||
use App\Space\PdfTemplateUtils;
|
||||
use App\Facades\PDF;
|
||||
use App\Traits\GeneratesPdfTrait;
|
||||
use App\Traits\HasCustomFieldsTrait;
|
||||
use Barryvdh\DomPDF\Facade\Pdf as PDF;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App;
|
||||
use App\Facades\PDF;
|
||||
use App\Mail\SendInvoiceMail;
|
||||
use App\Services\SerialNumberFormatter;
|
||||
use App\Space\PdfTemplateUtils;
|
||||
use App\Traits\GeneratesPdfTrait;
|
||||
use App\Traits\HasCustomFieldsTrait;
|
||||
use Barryvdh\DomPDF\Facade\Pdf as PDF;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
@@ -46,6 +46,15 @@ class SettingsPolicy
|
||||
return false;
|
||||
}
|
||||
|
||||
public function managePDFConfig(User $user)
|
||||
{
|
||||
if ($user->isOwner()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function manageSettings(User $user)
|
||||
{
|
||||
if ($user->isOwner()) {
|
||||
|
||||
@@ -127,6 +127,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
Gate::define('manage backups', [SettingsPolicy::class, 'manageBackups']);
|
||||
Gate::define('manage file disk', [SettingsPolicy::class, 'manageFileDisk']);
|
||||
Gate::define('manage email config', [SettingsPolicy::class, 'manageEmailConfig']);
|
||||
Gate::define('manage pdf config', [SettingsPolicy::class, 'managePDFConfig']);
|
||||
Gate::define('manage notes', [NotePolicy::class, 'manageNotes']);
|
||||
Gate::define('view notes', [NotePolicy::class, 'viewNotes']);
|
||||
|
||||
|
||||
13
app/Providers/PDFServiceProvider.php
Normal file
13
app/Providers/PDFServiceProvider.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Services\PDFService;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class PDFServiceProvider extends ServiceProvider
|
||||
{
|
||||
public $bindings = [
|
||||
'pdf.driver' => PDFService::class,
|
||||
];
|
||||
}
|
||||
59
app/Services/PDFDrivers/GotenbergPDFDriver.php
Normal file
59
app/Services/PDFDrivers/GotenbergPDFDriver.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\PDFDrivers;
|
||||
|
||||
use Gotenberg\Gotenberg;
|
||||
use Gotenberg\Stream;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class GotenbergPDFResponse
|
||||
{
|
||||
/** @var \Psr\Http\Message\ResponseInterface */
|
||||
protected $response;
|
||||
|
||||
public function __construct($stream)
|
||||
{
|
||||
$this->response = $stream;
|
||||
}
|
||||
|
||||
public function stream(string $filename = 'document.pdf'): Response
|
||||
{
|
||||
$output = $this->response->getBody();
|
||||
|
||||
return new Response($output, 200, [
|
||||
'Content-Type' => 'application/pdf',
|
||||
'Content-Disposition' => 'inline; filename="'.$filename.'"',
|
||||
]);
|
||||
}
|
||||
|
||||
public function output(): string
|
||||
{
|
||||
return $this->response->getBody()->getContents();
|
||||
}
|
||||
}
|
||||
|
||||
class GotenbergPDFDriver
|
||||
{
|
||||
public function loadView(string $viewname): GotenbergPDFResponse
|
||||
{
|
||||
$papersize = explode(' ', config('pdf.gotenberg.papersize'));
|
||||
if (count($papersize) != 2) {
|
||||
throw new \InvalidArgumentException('Invalid Gotenberg Papersize specified');
|
||||
}
|
||||
|
||||
$host = config('pdf.gotenberg.host');
|
||||
$request = Gotenberg::chromium($host)
|
||||
->pdf()
|
||||
->margins(0, 0, 0, 0) // Margins can be set using CSS
|
||||
->paperSize($papersize[0], $papersize[1])
|
||||
->html(
|
||||
Stream::string(
|
||||
'document.html',
|
||||
view($viewname)->render(),
|
||||
)
|
||||
);
|
||||
$result = Gotenberg::send($request);
|
||||
|
||||
return new GotenbergPDFResponse($result);
|
||||
}
|
||||
}
|
||||
47
app/Services/PDFService.php
Normal file
47
app/Services/PDFService.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
/*
|
||||
* Two options:
|
||||
* - Barryvdh\DomPDF\Facade\Pdf
|
||||
* - Gotenberg
|
||||
*/
|
||||
|
||||
use App;
|
||||
use App\Services\PDFDrivers\GotenbergPDFDriver;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
interface ResponseStream
|
||||
{
|
||||
public function stream(string $filename): Response;
|
||||
|
||||
public function output(): string;
|
||||
}
|
||||
|
||||
interface PDFDriver
|
||||
{
|
||||
public function loadView(string $template): ResponseStream;
|
||||
}
|
||||
|
||||
class PDFDriverFactory
|
||||
{
|
||||
public static function create(string $driver)
|
||||
{
|
||||
return match ($driver) {
|
||||
'dompdf' => App::make('dompdf.wrapper'),
|
||||
'gotenberg' => new GotenbergPDFDriver,
|
||||
default => throw new \InvalidArgumentException('Invalid PDFDriver requested')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class PDFService
|
||||
{
|
||||
public static function loadView(string $template)
|
||||
{
|
||||
$driver = config('pdf.driver');
|
||||
|
||||
return PDFDriverFactory::create($driver)->loadView($template);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use App\Http\Requests\DatabaseEnvironmentRequest;
|
||||
use App\Http\Requests\DiskEnvironmentRequest;
|
||||
use App\Http\Requests\DomainEnvironmentRequest;
|
||||
use App\Http\Requests\MailEnvironmentRequest;
|
||||
use App\Http\Requests\PDFConfigurationRequest;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -94,7 +95,6 @@ class EnvironmentManager
|
||||
}
|
||||
|
||||
return $str;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -225,7 +225,6 @@ class EnvironmentManager
|
||||
try {
|
||||
|
||||
$this->updateEnv($mailEnv);
|
||||
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'error' => 'mail_variables_save_error',
|
||||
@@ -237,6 +236,60 @@ class EnvironmentManager
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the pdf generation content to the .env file.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function savePDFVariables(PDFConfigurationRequest $request)
|
||||
{
|
||||
$pdfEnv = $this->getPDFConfiguration($request);
|
||||
|
||||
try {
|
||||
|
||||
$this->updateEnv($pdfEnv);
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'error' => 'pdf_variables_save_error',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => 'pdf_variables_save_successfully',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the pdf configuration
|
||||
*
|
||||
* @param PDFConfigurationRequest $request
|
||||
* @return array
|
||||
*/
|
||||
private function getPDFConfiguration($request)
|
||||
{
|
||||
$pdfEnv = [];
|
||||
|
||||
$driver = $request->get('pdf_driver');
|
||||
|
||||
switch ($driver) {
|
||||
case 'dompdf':
|
||||
$pdfEnv = [
|
||||
'PDF_DRIVER' => $request->get('pdf_driver'),
|
||||
];
|
||||
break;
|
||||
case 'gotenberg':
|
||||
$pdfEnv = [
|
||||
'PDF_DRIVER' => $request->get('pdf_driver'),
|
||||
'GOTENBERG_HOST' => $request->get('gotenberg_host'),
|
||||
'GOTENBERG_MARGINS' => $request->get('gotenberg_margins'),
|
||||
'GOTENBERG_PAPERSIZE' => $request->get('gotenberg_papersize'),
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
return $pdfEnv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mail configuration
|
||||
*
|
||||
@@ -316,7 +369,6 @@ class EnvironmentManager
|
||||
];
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
return $mailEnv;
|
||||
@@ -334,7 +386,6 @@ class EnvironmentManager
|
||||
try {
|
||||
|
||||
$this->updateEnv($diskEnv);
|
||||
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'error' => 'disk_variables_save_error',
|
||||
@@ -450,7 +501,6 @@ class EnvironmentManager
|
||||
}
|
||||
$formatted .= $current.$this->delimiter;
|
||||
$previous = $current;
|
||||
|
||||
}
|
||||
|
||||
file_put_contents($this->envPath, trim($formatted));
|
||||
|
||||
@@ -5,4 +5,5 @@ return [
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
App\Providers\DropboxServiceProvider::class,
|
||||
App\Providers\ViewServiceProvider::class,
|
||||
App\Providers\PDFServiceProvider::class,
|
||||
];
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"doctrine/dbal": "^4.2",
|
||||
"dragonmantank/cron-expression": "^v3.4",
|
||||
"guzzlehttp/guzzle": "^7.9",
|
||||
"gotenberg/gotenberg-php": "^2.8",
|
||||
"invoiceshelf/modules": "^1.0.0",
|
||||
"jasonmccreary/laravel-test-assertions": "^v2.4",
|
||||
"laravel/framework": "^11.31",
|
||||
@@ -79,7 +80,8 @@
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
|
||||
1323
composer.lock
generated
1323
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -156,6 +156,16 @@ return [
|
||||
'ability' => '',
|
||||
'model' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'settings.menu_title.pdf_generation',
|
||||
'group' => '',
|
||||
'name' => 'PDF Generation',
|
||||
'link' => '/admin/settings/pdf-generation',
|
||||
'icon' => 'DocumentIcon',
|
||||
'owner_only' => true,
|
||||
'ability' => '',
|
||||
'model' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'settings.roles.title',
|
||||
'group' => '',
|
||||
|
||||
14
config/pdf.php
Normal file
14
config/pdf.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'driver' => env('PDF_DRIVER', 'gotenberg'),
|
||||
|
||||
'gotenberg' => [
|
||||
'host' => env('GOTENBERG_HOST', 'http://pdf:3000'),
|
||||
'papersize' => env('GOTENBERG_PAPERSIZE', '210mm 297mm'),
|
||||
],
|
||||
|
||||
'dompdf' => [],
|
||||
|
||||
];
|
||||
13
lang/en.json
13
lang/en.json
@@ -852,7 +852,8 @@
|
||||
"payment_modes": "Payment Modes",
|
||||
"notes": "Notes",
|
||||
"exchange_rate": "Exchange Rate",
|
||||
"address_information": "Address Information"
|
||||
"address_information": "Address Information",
|
||||
"pdf_generation": "PDF Generation"
|
||||
},
|
||||
"address_information": {
|
||||
"section_description": " You can update Your Address information using form below."
|
||||
@@ -905,7 +906,15 @@
|
||||
"pdf": {
|
||||
"title": "PDF Setting",
|
||||
"footer_text": "Footer Text",
|
||||
"pdf_layout": "PDF Layout"
|
||||
"pdf_layout": "PDF Layout",
|
||||
"pdf_configuration": "PDF Generation Settings",
|
||||
"section_description": "Change the way PDFs are generated",
|
||||
"driver": "PDF Driver to use",
|
||||
"papersize": "Papersize",
|
||||
"papersize_hint": "Papersize in width and height (ex. \"210mm 297mm\")",
|
||||
"gotenberg_host": "Gotenberg service host",
|
||||
"pdf_variables_save_successfully": "PDF configuration saved successfully",
|
||||
"pdf_variables_save_error": "PDF configuration could not be saved"
|
||||
},
|
||||
"company_info": {
|
||||
"company_info": "Company info",
|
||||
|
||||
8
resources/scripts/admin/admin-router.js
vendored
8
resources/scripts/admin/admin-router.js
vendored
@@ -56,6 +56,8 @@ const UpdateApp = () =>
|
||||
import('@/scripts/admin/views/settings/UpdateAppSetting.vue')
|
||||
const RolesSettings = () =>
|
||||
import('@/scripts/admin/views/settings/RolesSettings.vue')
|
||||
const PDFGenerationSettings = () =>
|
||||
import('@/scripts/admin/views/settings/PDFGenerationSetting.vue')
|
||||
|
||||
// Items
|
||||
const ItemsIndex = () => import('@/scripts/admin/views/items/Index.vue')
|
||||
@@ -327,6 +329,12 @@ export default [
|
||||
meta: { isOwner: true },
|
||||
component: UpdateApp,
|
||||
},
|
||||
{
|
||||
path: 'pdf-generation',
|
||||
name: 'pdf.generation',
|
||||
meta: { isOwner: true },
|
||||
component: PDFGenerationSettings,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
70
resources/scripts/admin/stores/pdf-driver.js
vendored
Normal file
70
resources/scripts/admin/stores/pdf-driver.js
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
const { defineStore } = window.pinia
|
||||
import { handleError } from '@/scripts/helpers/error-handling'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import axios from 'axios'
|
||||
|
||||
export const usePDFDriverStore = (useWindow = false) => {
|
||||
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
|
||||
const { global } = window.i18n
|
||||
|
||||
return defineStoreFunc({
|
||||
id: 'pdf-driver',
|
||||
|
||||
state: () => ({
|
||||
pdfDriverConfig: null,
|
||||
pdf_driver: 'dompdf',
|
||||
pdf_drivers: [],
|
||||
|
||||
dompdf: {
|
||||
pdf_driver: '',
|
||||
},
|
||||
gotenberg: {
|
||||
pdf_driver: '',
|
||||
gotenberg_host: '',
|
||||
gotenberg_papersize: ''
|
||||
}
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async fetchDrivers() {
|
||||
try {
|
||||
const response = await axios.get('/api/v1/pdf/drivers')
|
||||
this.pdf_drivers = response.data
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
async fetchConfig() {
|
||||
try {
|
||||
const response = await axios.get('/api/v1/pdf/config')
|
||||
this.pdfDriverConfig = response.data
|
||||
this.pdf_driver = response.data.pdf_driver
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
async updateConfig(data) {
|
||||
try {
|
||||
const response = await axios.post('/api/v1/pdf/config', data)
|
||||
const notificationStore = useNotificationStore()
|
||||
if (response.data.success) {
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: global.t('settings.pdf.' + response.data.success),
|
||||
})
|
||||
} else {
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: global.t('settings.pdf.' + response.data.error),
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<BaseSettingCard
|
||||
:title="$t('settings.pdf.pdf_configuration')"
|
||||
:description="$t('settings.pdf.section_description')"
|
||||
>
|
||||
<div v-if="pdfDriverStore && pdfDriverStore.pdfDriverConfig" class="mt-14">
|
||||
<component
|
||||
:is="pdfDriver"
|
||||
:config-data="pdfDriverStore.pdfDriverConfig"
|
||||
:is-saving="isSaving"
|
||||
:drivers="pdfDriverStore.pdf_drivers"
|
||||
:is-fetching-initial-data="isFetchingInitialData"
|
||||
@on-change-driver="(val) => changeDriver(val)"
|
||||
@submit-data="saveConfig"
|
||||
>
|
||||
</component>
|
||||
</div>
|
||||
</BaseSettingCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import GotenbergDriver from '@/scripts/admin/views/settings/pdf-driver/GotenbergDriver.vue';
|
||||
import DomPDFDriver from '@/scripts/admin/views/settings/pdf-driver/DomPDFDriver.vue';
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { usePDFDriverStore } from '@/scripts/admin/stores/pdf-driver'
|
||||
const emit = defineEmits(['submit-data', 'on-change-driver'])
|
||||
|
||||
let isFetchingInitialData = ref(false)
|
||||
let isSaving = ref(false)
|
||||
|
||||
const pdfDriverStore = usePDFDriverStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
function changeDriver(value) {
|
||||
pdfDriverStore.pdf_driver = value
|
||||
pdfDriverStore.pdfDriverConfig.pdf_driver = value
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
isFetchingInitialData.value = true
|
||||
await Promise.all([
|
||||
pdfDriverStore.fetchDrivers(),
|
||||
pdfDriverStore.fetchConfig(),
|
||||
])
|
||||
isFetchingInitialData.value = false
|
||||
}
|
||||
loadData();
|
||||
|
||||
async function saveConfig(value) {
|
||||
try {
|
||||
isSaving.value = true
|
||||
await pdfDriverStore.updateConfig(value)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const pdfDriver = computed(() => {
|
||||
switch (pdfDriverStore.pdf_driver) {
|
||||
case 'dompdf':
|
||||
return DomPDFDriver
|
||||
case 'gotenberg':
|
||||
return GotenbergDriver
|
||||
default:
|
||||
return DomPDFDriver
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<form @submit.prevent="saveConfig">
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.pdf.driver')"
|
||||
:error="
|
||||
v$.dompdf.pdf_driver.$error &&
|
||||
v$.dompdf.pdf_driver.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="pdfDriverStore.dompdf.pdf_driver"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:options="drivers"
|
||||
:can-deselect="false"
|
||||
:invalid="v$.dompdf.pdf_driver.$error"
|
||||
@update:modelValue="onChangeDriver"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
<div class="flex my-10">
|
||||
<BaseButton
|
||||
:disabled="isSaving"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:loading="isSaving"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon v-if="!isSaving" name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
<slot />
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { usePDFDriverStore } from '@/scripts/admin/stores/pdf-driver'
|
||||
import {required, email, helpers} from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
|
||||
const props = defineProps({
|
||||
configData: {
|
||||
type: Object,
|
||||
require: true,
|
||||
default: Object,
|
||||
},
|
||||
isSaving: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
isFetchingInitialData: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
drivers: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const emit = defineEmits(['submit-data', 'on-change-driver'])
|
||||
|
||||
let isFetchingInitialData = ref(false)
|
||||
|
||||
const pdfDriverStore = usePDFDriverStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
dompdf: {
|
||||
pdf_driver: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => pdfDriverStore)
|
||||
)
|
||||
|
||||
function onChangeDriver() {
|
||||
// validation
|
||||
v$.value.dompdf.pdf_driver.$touch()
|
||||
emit('on-change-driver', pdfDriverStore.dompdf.pdf_driver)
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
v$.value.dompdf.$touch()
|
||||
if (!v$.value.dompdf.$invalid) {
|
||||
emit('submit-data', pdfDriverStore.dompdf)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
for (const key in pdfDriverStore.dompdf) {
|
||||
if (props.configData.hasOwnProperty(key)) {
|
||||
pdfDriverStore.$patch((state) => {
|
||||
state.dompdf[key] = props.configData[key]
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<form @submit.prevent="saveConfig">
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup :label="$t('settings.pdf.driver')" :error="
|
||||
v$.gotenberg.pdf_driver.$error &&
|
||||
v$.gotenberg.pdf_driver.$errors[0].$message
|
||||
" required>
|
||||
<BaseMultiselect v-model="pdfDriverStore.gotenberg.pdf_driver" :content-loading="isFetchingInitialData"
|
||||
:options="drivers" :can-deselect="false" @update:modelValue="onChangeDriver"
|
||||
:invalid="v$.gotenberg.pdf_driver.$error" />
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup :label="$t('settings.pdf.gotenberg_host')" :content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.gotenberg.gotenberg_host.$error &&
|
||||
v$.gotenberg.gotenberg_host.$errors[0].$message
|
||||
" required>
|
||||
<BaseInput v-model="pdfDriverStore.gotenberg.gotenberg_host" :content-loading="isFetchingInitialData"
|
||||
:invalid="v$.gotenberg.gotenberg_host.$error" type="text" name="gotenberg_host" />
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup :label="$t('settings.pdf.papersize')" :content-loading="isFetchingInitialData" :error="
|
||||
v$.gotenberg.gotenberg_papersize.$error &&
|
||||
v$.gotenberg.gotenberg_papersize.$errors[0].$message
|
||||
"
|
||||
:help-text="$t('settings.pdf.papersize_hint')"
|
||||
required>
|
||||
<BaseInput v-model="pdfDriverStore.gotenberg.gotenberg_papersize" :content-loading="isFetchingInitialData"
|
||||
:invalid="v$.gotenberg.gotenberg_papersize.$error" type="text" name="gotenberg_papersize" />
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
<div class="flex my-10">
|
||||
<BaseButton :disabled="isSaving" :content-loading="isFetchingInitialData" :loading="isSaving" type="submit"
|
||||
variant="primary">
|
||||
<template #left="slotProps">
|
||||
<BaseIcon v-if="!isSaving" name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
<slot />
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, computed, onMounted} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {usePDFDriverStore} from '@/scripts/admin/stores/pdf-driver'
|
||||
import {required, email, helpers} from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
|
||||
const props = defineProps({
|
||||
configData: {
|
||||
type: Object,
|
||||
require: true,
|
||||
default: Object,
|
||||
},
|
||||
isSaving: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
isFetchingInitialData: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
drivers: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const emit = defineEmits(['submit-data', 'on-change-driver'])
|
||||
|
||||
const pdfDriverStore = usePDFDriverStore();
|
||||
const {t} = useI18n();
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
gotenberg: {
|
||||
pdf_driver: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
gotenberg_host: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
gotenberg_papersize: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => pdfDriverStore)
|
||||
)
|
||||
|
||||
function onChangeDriver() {
|
||||
v$.value.gotenberg.pdf_driver.$touch()
|
||||
emit('on-change-driver', pdfDriverStore.gotenberg.pdf_driver)
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
v$.value.gotenberg.$touch()
|
||||
if (!v$.value.gotenberg.$invalid) {
|
||||
emit('submit-data', pdfDriverStore.gotenberg)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Fill pdfDriverStore.gotenbergConfig with data from config prop
|
||||
onMounted(() => {
|
||||
for (const key in pdfDriverStore.gotenberg) {
|
||||
if (props.configData.hasOwnProperty(key)) {
|
||||
pdfDriverStore.$patch((state) => {
|
||||
state.gotenberg[key] = props.configData[key]
|
||||
});
|
||||
}
|
||||
}
|
||||
if (pdfDriverStore.gotenberg.gotenberg_papersize == '')
|
||||
pdfDriverStore.$patch((state) => {
|
||||
state.gotenberg.gotenberg_papersize = '210mm 297mm';
|
||||
});
|
||||
})
|
||||
|
||||
</script>
|
||||
@@ -76,6 +76,7 @@ use App\Http\Controllers\V1\Admin\Settings\GetCompanySettingsController;
|
||||
use App\Http\Controllers\V1\Admin\Settings\GetSettingsController;
|
||||
use App\Http\Controllers\V1\Admin\Settings\GetUserSettingsController;
|
||||
use App\Http\Controllers\V1\Admin\Settings\MailConfigurationController;
|
||||
use App\Http\Controllers\V1\Admin\Settings\PDFConfigurationController;
|
||||
use App\Http\Controllers\V1\Admin\Settings\TaxTypesController;
|
||||
use App\Http\Controllers\V1\Admin\Settings\UpdateCompanySettingsController;
|
||||
use App\Http\Controllers\V1\Admin\Settings\UpdateSettingsController;
|
||||
@@ -397,6 +398,15 @@ Route::prefix('/v1')->group(function () {
|
||||
|
||||
Route::get('/company/mail/config', GetCompanyMailConfigurationController::class);
|
||||
|
||||
// PDF Generation
|
||||
//----------------------------------
|
||||
|
||||
Route::get('/pdf/drivers', [PDFConfigurationController::class, 'getDrivers']);
|
||||
|
||||
Route::get('/pdf/config', [PDFConfigurationController::class, 'getEnvironment']);
|
||||
|
||||
Route::post('/pdf/config', [PDFConfigurationController::class, 'saveEnvironment']);
|
||||
|
||||
Route::apiResource('notes', NotesController::class);
|
||||
|
||||
// Tax Types
|
||||
|
||||
Reference in New Issue
Block a user