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:
Tim van Osch
2025-05-04 02:10:15 +02:00
committed by GitHub
parent 8a9392e400
commit bf40f792c2
27 changed files with 1512 additions and 597 deletions

View File

@@ -63,6 +63,11 @@ services:
networks: networks:
- invoiceshelf-dev - invoiceshelf-dev
pdf:
image: gotenberg/gotenberg:8
networks:
- invoiceshelf-dev
networks: networks:
invoiceshelf-dev: invoiceshelf-dev:

View File

@@ -62,6 +62,11 @@ services:
networks: networks:
- invoiceshelf-dev - invoiceshelf-dev
pdf:
image: gotenberg/gotenberg:8
networks:
- invoiceshelf-dev
networks: networks:
invoiceshelf-dev: invoiceshelf-dev:

View File

@@ -50,5 +50,10 @@ services:
networks: networks:
- invoiceshelf-dev - invoiceshelf-dev
pdf:
image: gotenberg/gotenberg:8
networks:
- invoiceshelf-dev
networks: networks:
invoiceshelf-dev: invoiceshelf-dev:

View File

@@ -54,3 +54,7 @@ TRUSTED_PROXIES="*"
CRON_JOB_AUTH_TOKEN="" CRON_JOB_AUTH_TOKEN=""
LOG_STACK=single LOG_STACK=single
PDF_DRIVER=dompdf
GOTENBERG_HOST=
GOTENBERG_PAPERSIZE=

16
app/Facades/PDF.php Normal file
View 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';
}
}

View File

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

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

View File

@@ -6,9 +6,9 @@ use App;
use App\Mail\SendEstimateMail; use App\Mail\SendEstimateMail;
use App\Services\SerialNumberFormatter; use App\Services\SerialNumberFormatter;
use App\Space\PdfTemplateUtils; use App\Space\PdfTemplateUtils;
use App\Facades\PDF;
use App\Traits\GeneratesPdfTrait; use App\Traits\GeneratesPdfTrait;
use App\Traits\HasCustomFieldsTrait; use App\Traits\HasCustomFieldsTrait;
use Barryvdh\DomPDF\Facade\Pdf as PDF;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;

View File

@@ -3,12 +3,12 @@
namespace App\Models; namespace App\Models;
use App; use App;
use App\Facades\PDF;
use App\Mail\SendInvoiceMail; use App\Mail\SendInvoiceMail;
use App\Services\SerialNumberFormatter; use App\Services\SerialNumberFormatter;
use App\Space\PdfTemplateUtils; use App\Space\PdfTemplateUtils;
use App\Traits\GeneratesPdfTrait; use App\Traits\GeneratesPdfTrait;
use App\Traits\HasCustomFieldsTrait; use App\Traits\HasCustomFieldsTrait;
use Barryvdh\DomPDF\Facade\Pdf as PDF;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;

View File

@@ -46,6 +46,15 @@ class SettingsPolicy
return false; return false;
} }
public function managePDFConfig(User $user)
{
if ($user->isOwner()) {
return true;
}
return false;
}
public function manageSettings(User $user) public function manageSettings(User $user)
{ {
if ($user->isOwner()) { if ($user->isOwner()) {

View File

@@ -127,6 +127,7 @@ class AppServiceProvider extends ServiceProvider
Gate::define('manage backups', [SettingsPolicy::class, 'manageBackups']); Gate::define('manage backups', [SettingsPolicy::class, 'manageBackups']);
Gate::define('manage file disk', [SettingsPolicy::class, 'manageFileDisk']); Gate::define('manage file disk', [SettingsPolicy::class, 'manageFileDisk']);
Gate::define('manage email config', [SettingsPolicy::class, 'manageEmailConfig']); 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('manage notes', [NotePolicy::class, 'manageNotes']);
Gate::define('view notes', [NotePolicy::class, 'viewNotes']); Gate::define('view notes', [NotePolicy::class, 'viewNotes']);

View 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,
];
}

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

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

View File

@@ -6,6 +6,7 @@ use App\Http\Requests\DatabaseEnvironmentRequest;
use App\Http\Requests\DiskEnvironmentRequest; use App\Http\Requests\DiskEnvironmentRequest;
use App\Http\Requests\DomainEnvironmentRequest; use App\Http\Requests\DomainEnvironmentRequest;
use App\Http\Requests\MailEnvironmentRequest; use App\Http\Requests\MailEnvironmentRequest;
use App\Http\Requests\PDFConfigurationRequest;
use Exception; use Exception;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@@ -94,7 +95,6 @@ class EnvironmentManager
} }
return $str; return $str;
} }
/** /**
@@ -225,7 +225,6 @@ class EnvironmentManager
try { try {
$this->updateEnv($mailEnv); $this->updateEnv($mailEnv);
} catch (Exception $e) { } catch (Exception $e) {
return [ return [
'error' => 'mail_variables_save_error', '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 * Returns the mail configuration
* *
@@ -316,7 +369,6 @@ class EnvironmentManager
]; ];
break; break;
} }
return $mailEnv; return $mailEnv;
@@ -334,7 +386,6 @@ class EnvironmentManager
try { try {
$this->updateEnv($diskEnv); $this->updateEnv($diskEnv);
} catch (Exception $e) { } catch (Exception $e) {
return [ return [
'error' => 'disk_variables_save_error', 'error' => 'disk_variables_save_error',
@@ -450,7 +501,6 @@ class EnvironmentManager
} }
$formatted .= $current.$this->delimiter; $formatted .= $current.$this->delimiter;
$previous = $current; $previous = $current;
} }
file_put_contents($this->envPath, trim($formatted)); file_put_contents($this->envPath, trim($formatted));

View File

@@ -5,4 +5,5 @@ return [
App\Providers\RouteServiceProvider::class, App\Providers\RouteServiceProvider::class,
App\Providers\DropboxServiceProvider::class, App\Providers\DropboxServiceProvider::class,
App\Providers\ViewServiceProvider::class, App\Providers\ViewServiceProvider::class,
App\Providers\PDFServiceProvider::class,
]; ];

View File

@@ -14,6 +14,7 @@
"doctrine/dbal": "^4.2", "doctrine/dbal": "^4.2",
"dragonmantank/cron-expression": "^v3.4", "dragonmantank/cron-expression": "^v3.4",
"guzzlehttp/guzzle": "^7.9", "guzzlehttp/guzzle": "^7.9",
"gotenberg/gotenberg-php": "^2.8",
"invoiceshelf/modules": "^1.0.0", "invoiceshelf/modules": "^1.0.0",
"jasonmccreary/laravel-test-assertions": "^v2.4", "jasonmccreary/laravel-test-assertions": "^v2.4",
"laravel/framework": "^11.31", "laravel/framework": "^11.31",
@@ -79,7 +80,8 @@
"preferred-install": "dist", "preferred-install": "dist",
"sort-packages": true, "sort-packages": true,
"allow-plugins": { "allow-plugins": {
"pestphp/pest-plugin": true "pestphp/pest-plugin": true,
"php-http/discovery": true
} }
}, },
"extra": { "extra": {

1323
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -156,6 +156,16 @@ return [
'ability' => '', 'ability' => '',
'model' => '', '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', 'title' => 'settings.roles.title',
'group' => '', 'group' => '',

14
config/pdf.php Normal file
View 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' => [],
];

View File

@@ -852,7 +852,8 @@
"payment_modes": "Payment Modes", "payment_modes": "Payment Modes",
"notes": "Notes", "notes": "Notes",
"exchange_rate": "Exchange Rate", "exchange_rate": "Exchange Rate",
"address_information": "Address Information" "address_information": "Address Information",
"pdf_generation": "PDF Generation"
}, },
"address_information": { "address_information": {
"section_description": " You can update Your Address information using form below." "section_description": " You can update Your Address information using form below."
@@ -905,7 +906,15 @@
"pdf": { "pdf": {
"title": "PDF Setting", "title": "PDF Setting",
"footer_text": "Footer Text", "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": "Company info", "company_info": "Company info",

View File

@@ -56,6 +56,8 @@ const UpdateApp = () =>
import('@/scripts/admin/views/settings/UpdateAppSetting.vue') import('@/scripts/admin/views/settings/UpdateAppSetting.vue')
const RolesSettings = () => const RolesSettings = () =>
import('@/scripts/admin/views/settings/RolesSettings.vue') import('@/scripts/admin/views/settings/RolesSettings.vue')
const PDFGenerationSettings = () =>
import('@/scripts/admin/views/settings/PDFGenerationSetting.vue')
// Items // Items
const ItemsIndex = () => import('@/scripts/admin/views/items/Index.vue') const ItemsIndex = () => import('@/scripts/admin/views/items/Index.vue')
@@ -327,6 +329,12 @@ export default [
meta: { isOwner: true }, meta: { isOwner: true },
component: UpdateApp, component: UpdateApp,
}, },
{
path: 'pdf-generation',
name: 'pdf.generation',
meta: { isOwner: true },
component: PDFGenerationSettings,
},
], ],
}, },

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

View File

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

View File

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

View File

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

View File

@@ -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\GetSettingsController;
use App\Http\Controllers\V1\Admin\Settings\GetUserSettingsController; use App\Http\Controllers\V1\Admin\Settings\GetUserSettingsController;
use App\Http\Controllers\V1\Admin\Settings\MailConfigurationController; 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\TaxTypesController;
use App\Http\Controllers\V1\Admin\Settings\UpdateCompanySettingsController; use App\Http\Controllers\V1\Admin\Settings\UpdateCompanySettingsController;
use App\Http\Controllers\V1\Admin\Settings\UpdateSettingsController; use App\Http\Controllers\V1\Admin\Settings\UpdateSettingsController;
@@ -397,6 +398,15 @@ Route::prefix('/v1')->group(function () {
Route::get('/company/mail/config', GetCompanyMailConfigurationController::class); 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); Route::apiResource('notes', NotesController::class);
// Tax Types // Tax Types